/**
 * \file session_client.c
 * \author Michal Vasko <mvasko@cesnet.cz>
 * \brief libnetconf2 session client functions
 *
 * Copyright (c) 2015 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

#ifdef __linux__
# include <sys/syscall.h>
#endif

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/select.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <poll.h>
#include <pwd.h>

#include <libyang/libyang.h>

#include "compat.h"
#include "libnetconf.h"
#include "session_client.h"
#include "messages_client.h"

#include "../modules/ietf_netconf_monitoring@2010-10-04_yang.h"
#include "../modules/ietf_netconf@2013-09-29_yang.h"

static const char *ncds2str[] = {NULL, "config", "url", "running", "startup", "candidate"};

#ifdef NC_ENABLED_SSH
int sshauth_hostkey_check(const char *hostname, ssh_session session, void *priv);
char *sshauth_password(const char *username, const char *hostname, void *priv);
char *sshauth_interactive(const char *auth_name, const char *instruction, const char *prompt, int echo, void *priv);
char *sshauth_privkey_passphrase(const char* privkey_path, void *priv);
#endif /* NC_ENABLED_SSH */

static pthread_once_t nc_client_context_once = PTHREAD_ONCE_INIT;
static pthread_key_t nc_client_context_key;
#ifdef __linux__
static struct nc_client_context context_main = {
    .opts.ka = {
        .enabled = 1,
        .idle_time = 1,
        .max_probes = 10,
        .probe_interval = 5
    },
#ifdef NC_ENABLED_SSH
    .ssh_opts = {
        .auth_pref = {{NC_SSH_AUTH_INTERACTIVE, 3}, {NC_SSH_AUTH_PASSWORD, 2}, {NC_SSH_AUTH_PUBLICKEY, 1}},
        .auth_hostkey_check = sshauth_hostkey_check,
        .auth_password = sshauth_password,
        .auth_interactive = sshauth_interactive,
        .auth_privkey_passphrase = sshauth_privkey_passphrase
    },
    .ssh_ch_opts = {
        .auth_pref = {{NC_SSH_AUTH_INTERACTIVE, 1}, {NC_SSH_AUTH_PASSWORD, 2}, {NC_SSH_AUTH_PUBLICKEY, 3}},
        .auth_hostkey_check = sshauth_hostkey_check,
        .auth_password = sshauth_password,
        .auth_interactive = sshauth_interactive,
        .auth_privkey_passphrase = sshauth_privkey_passphrase
    },
#endif /* NC_ENABLED_SSH */
    /* .tls_ structures zeroed */
    .refcount = 0
};
#endif

static void
nc_client_context_free(void *ptr)
{
    struct nc_client_context *c = (struct nc_client_context *)ptr;

    if (--(c->refcount)) {
        /* still used */
        return;
    }

#ifdef __linux__
    /* in __linux__ we use static memory in the main thread,
     * so this check is for programs terminating the main()
     * function by pthread_exit() :)
     */
    if (c != &context_main)
#endif
    {
        /* for the main thread the same is done in nc_client_destroy() */
        free(c->opts.schema_searchpath);

#if defined(NC_ENABLED_SSH) || defined(NC_ENABLED_TLS)
        int i;
        for (i = 0; i < c->opts.ch_bind_count; ++i) {
            close(c->opts.ch_binds[i].sock);
            free((char *)c->opts.ch_binds[i].address);
        }
        free(c->opts.ch_binds);
        c->opts.ch_binds = NULL;
        c->opts.ch_bind_count = 0;
#endif
#ifdef NC_ENABLED_SSH
        _nc_client_ssh_destroy_opts(&c->ssh_opts);
        _nc_client_ssh_destroy_opts(&c->ssh_ch_opts);
#endif
#ifdef NC_ENABLED_TLS
        _nc_client_tls_destroy_opts(&c->tls_opts);
        _nc_client_tls_destroy_opts(&c->tls_ch_opts);
#endif
        free(c);
    }
}

static void
nc_client_context_createkey(void)
{
    int r;

    /* initiate */
    while ((r = pthread_key_create(&nc_client_context_key, nc_client_context_free)) == EAGAIN);
    pthread_setspecific(nc_client_context_key, NULL);
}

struct nc_client_context *
nc_client_context_location(void)
{
    struct nc_client_context *e;

    pthread_once(&nc_client_context_once, nc_client_context_createkey);
    e = pthread_getspecific(nc_client_context_key);
    if (!e) {
        /* prepare ly_err storage */
#ifdef __linux__
        if (getpid() == syscall(SYS_gettid)) {
            /* main thread - use global variable instead of thread-specific variable. */
            e = &context_main;
        } else
#endif /* __linux__ */
        {
            e = calloc(1, sizeof *e);
            /* set default values */
            e->refcount = 1;
#ifdef NC_ENABLED_SSH
            e->ssh_opts.auth_pref[0].type = NC_SSH_AUTH_INTERACTIVE;
            e->ssh_opts.auth_pref[0].value = 3;
            e->ssh_opts.auth_pref[1].type = NC_SSH_AUTH_PASSWORD;
            e->ssh_opts.auth_pref[1].value = 2;
            e->ssh_opts.auth_pref[2].type = NC_SSH_AUTH_PUBLICKEY;
            e->ssh_opts.auth_pref[2].value = 1;
            e->ssh_opts.auth_hostkey_check = sshauth_hostkey_check;
            e->ssh_opts.auth_password = sshauth_password;
            e->ssh_opts.auth_interactive = sshauth_interactive;
            e->ssh_opts.auth_privkey_passphrase = sshauth_privkey_passphrase;

            /* callhome settings are the same except the inverted auth methods preferences */
            memcpy(&e->ssh_ch_opts, &e->ssh_opts, sizeof e->ssh_ch_opts);
            e->ssh_ch_opts.auth_pref[0].value = 1;
            e->ssh_ch_opts.auth_pref[1].value = 2;
            e->ssh_ch_opts.auth_pref[2].value = 3;
#endif /* NC_ENABLED_SSH */
        }
        pthread_setspecific(nc_client_context_key, e);
    }

    return e;
}

#define client_opts nc_client_context_location()->opts

API void *
nc_client_get_thread_context(void)
{
    return nc_client_context_location();
}

API void
nc_client_set_thread_context(void *context)
{
    struct nc_client_context *old, *new;

    if (!context) {
        ERRARG(context);
        return;
    }

    new = (struct nc_client_context *)context;
    old = nc_client_context_location();
    if (old == new) {
        /* nothing to change */
        return;
    }

    /* replace old by new, increase reference counter in the newly set context */
    nc_client_context_free(old);
    new->refcount++;
    pthread_setspecific(nc_client_context_key, new);
}

int
nc_session_new_ctx(struct nc_session *session, struct ly_ctx *ctx)
{
    /* assign context (dicionary needed for handshake) */
    if (!ctx) {
        ctx = ly_ctx_new(NULL, LY_CTX_NOYANGLIBRARY);
        if (!ctx) {
            return EXIT_FAILURE;
        }

        /* user path must be first, the first path is used to store schemas retreived via get-schema */
        if (client_opts.schema_searchpath) {
            ly_ctx_set_searchdir(ctx, client_opts.schema_searchpath);
        } else if (!access(NC_YANG_DIR, F_OK)) {
            ly_ctx_set_searchdir(ctx, NC_YANG_DIR);
        }

        /* set callback for getting schemas, if provided */
        ly_ctx_set_module_imp_clb(ctx, client_opts.schema_clb, client_opts.schema_clb_data);
    } else {
        session->flags |= NC_SESSION_SHAREDCTX;
    }

    session->ctx = ctx;

    return EXIT_SUCCESS;
}

API int
nc_client_set_schema_searchpath(const char *path)
{
    if (client_opts.schema_searchpath) {
        free(client_opts.schema_searchpath);
    }

    if (path) {
        client_opts.schema_searchpath = strdup(path);
        if (!client_opts.schema_searchpath) {
            ERRMEM;
            return 1;
        }
    } else {
        client_opts.schema_searchpath = NULL;
    }

    return 0;
}

API const char *
nc_client_get_schema_searchpath(void)
{
    return client_opts.schema_searchpath;
}

API int
nc_client_set_schema_callback(ly_module_imp_clb clb, void *user_data)
{
    client_opts.schema_clb = clb;
    if (clb) {
        client_opts.schema_clb_data = user_data;
    } else {
        client_opts.schema_clb_data = NULL;
    }

    return 0;
}

API ly_module_imp_clb
nc_client_get_schema_callback(void **user_data)
{
    if (user_data) {
        (*user_data) = client_opts.schema_clb_data;
    }
    return client_opts.schema_clb;
}


struct schema_info {
    char *name;
    char *revision;
    struct {
        char *name;
        char *revision;
    } *submodules;
    struct ly_set features;
    int implemented;
};

struct clb_data_s {
    void *user_data;
    ly_module_imp_clb user_clb;
    struct schema_info *schemas;
    struct nc_session *session;
    int has_get_schema;
};

static char *
retrieve_schema_data_localfile(const char *name, const char *rev, struct clb_data_s *clb_data,
                               LYS_INFORMAT *format)
{
    char *localfile = NULL;
    FILE *f;
    long length, l;
    char *model_data = NULL;

    if (lys_search_localfile(ly_ctx_get_searchdirs(clb_data->session->ctx),
                             !(ly_ctx_get_options(clb_data->session->ctx) & LY_CTX_DISABLE_SEARCHDIR_CWD),
                             name, rev, &localfile, format)) {
        return NULL;
    }
    if (localfile) {
        VRB("Session %u: reading schema from localfile \"%s\".", clb_data->session->id, localfile);
        f = fopen(localfile, "r");
        if (!f) {
            ERR("Session %u: unable to open \"%s\" file to get schema (%s).",
                clb_data->session->id, localfile, strerror(errno));
            free(localfile);
            return NULL;
        }

        fseek(f, 0, SEEK_END);
        length = ftell(f);
        if (length < 0) {
            ERR("Session %u: unable to get size of schema file \"%s\".",
                clb_data->session->id, localfile);
            free(localfile);
            fclose(f);
            return NULL;
        }
        fseek(f, 0, SEEK_SET);

        model_data = malloc(length + 1);
        if (!model_data) {
            ERRMEM;
        } else if ((l = fread(model_data, 1, length, f)) != length) {
            ERR("Session %u: reading schema from \"%s\" failed (%d bytes read, but %d expected).",
                clb_data->session->id, localfile, l, length);
            free(model_data);
            model_data = NULL;
        } else {
            /* terminating NULL byte */
            model_data[length] = '\0';
        }
        fclose(f);
        free(localfile);
    }

    return model_data;
}

static char *
retrieve_schema_data_getschema(const char *name, const char *rev, struct clb_data_s *clb_data,
                               LYS_INFORMAT *format)
{
    struct nc_rpc *rpc;
    struct nc_reply *reply;
    struct nc_reply_data *data_rpl;
    struct nc_reply_error *error_rpl;
    struct lyd_node_anydata *get_schema_data;
    NC_MSG_TYPE msg;
    uint64_t msgid;
    char *localfile = NULL;
    FILE *f;
    char *model_data = NULL;

    VRB("Session %u: reading schema from server via get-schema.", clb_data->session->id);
    rpc = nc_rpc_getschema(name, rev, "yang", NC_PARAMTYPE_CONST);

    while ((msg = nc_send_rpc(clb_data->session, rpc, 0, &msgid)) == NC_MSG_WOULDBLOCK) {
        usleep(1000);
    }
    if (msg == NC_MSG_ERROR) {
        ERR("Session %u: failed to send the <get-schema> RPC.", clb_data->session->id);
        nc_rpc_free(rpc);
        return NULL;
    }

    do {
        msg = nc_recv_reply(clb_data->session, rpc, msgid, NC_READ_ACT_TIMEOUT * 1000, 0, &reply);
    } while (msg == NC_MSG_NOTIF || msg == NC_MSG_REPLY_ERR_MSGID);
    nc_rpc_free(rpc);
    if (msg == NC_MSG_WOULDBLOCK) {
        ERR("Session %u: timeout for receiving reply to a <get-schema> expired.", clb_data->session->id);
        return NULL;
    } else if (msg == NC_MSG_ERROR || reply == NULL) {
        ERR("Session %u: failed to receive a reply to <get-schema>.", clb_data->session->id);
        return NULL;
    }

    switch (reply->type) {
    case NC_RPL_OK:
        ERR("Session %u: unexpected reply OK to a <get-schema> RPC.", clb_data->session->id);
        nc_reply_free(reply);
        return NULL;
    case NC_RPL_DATA:
        /* fine */
        break;
    case NC_RPL_ERROR:
        error_rpl = (struct nc_reply_error *)reply;
        if (error_rpl->count) {
            ERR("Session %u: error reply to a <get-schema> RPC (tag \"%s\", message \"%s\").",
                clb_data->session->id, error_rpl->err[0].tag, error_rpl->err[0].message);
        } else {
            ERR("Session %u: unexpected reply error to a <get-schema> RPC.", clb_data->session->id);
        }
        nc_reply_free(reply);
        return NULL;
    case NC_RPL_NOTIF:
        ERR("Session %u: unexpected reply notification to a <get-schema> RPC.", clb_data->session->id);
        nc_reply_free(reply);
        return NULL;
    }

    data_rpl = (struct nc_reply_data *)reply;
    if (!data_rpl->data || (data_rpl->data->schema->nodetype != LYS_RPC) || strcmp(data_rpl->data->schema->name, "get-schema")
            || !data_rpl->data->child || (data_rpl->data->child->schema->nodetype != LYS_ANYXML)) {
        ERR("Session %u: unexpected data in reply to a <get-schema> RPC.", clb_data->session->id);
        nc_reply_free(reply);
        return NULL;
    }
    get_schema_data = (struct lyd_node_anydata *)data_rpl->data->child;
    switch (get_schema_data->value_type) {
    case LYD_ANYDATA_CONSTSTRING:
    case LYD_ANYDATA_STRING:
        model_data = strdup(get_schema_data->value.str);
        break;
    case LYD_ANYDATA_DATATREE:
        lyd_print_mem(&model_data, get_schema_data->value.tree, LYD_XML, LYP_WITHSIBLINGS);
        break;
    case LYD_ANYDATA_XML:
        lyxml_print_mem(&model_data, get_schema_data->value.xml, LYXML_PRINT_SIBLINGS);
        break;
    case LYD_ANYDATA_JSON:
    case LYD_ANYDATA_JSOND:
    case LYD_ANYDATA_SXML:
    case LYD_ANYDATA_SXMLD:
    case LYD_ANYDATA_LYB:
    case LYD_ANYDATA_LYBD:
        ERRINT;
        nc_reply_free(reply);
    }
    nc_reply_free(reply);

    if (model_data && !model_data[0]) {
        /* empty data */
        free(model_data);
        model_data = NULL;
    }

    /* try to store the model_data into local schema repository */
    if (model_data) {
        *format = LYS_IN_YANG;
        if (client_opts.schema_searchpath) {
            if (asprintf(&localfile, "%s/%s%s%s.yang", client_opts.schema_searchpath, name,
                         rev ? "@" : "", rev ? rev : "") == -1) {
                ERRMEM;
            } else {
                f = fopen(localfile, "w");
                if (!f) {
                    WRN("Unable to store \"%s\" as a local copy of schema retrieved via <get-schema> (%s).",
                        localfile, strerror(errno));
                } else {
                    fputs(model_data, f);
                    fclose(f);
                }
                free(localfile);
            }
        }
    }

    return model_data;
}

static void free_with_user_data(void *data, void *user_data)
{
    free(data);
    (void)user_data;
}

static const char *
retrieve_schema_data(const char *mod_name, const char *mod_rev, const char *submod_name, const char *sub_rev,
                     void *user_data, LYS_INFORMAT *format, void (**free_module_data)(void *model_data, void *user_data))
{
    struct clb_data_s *clb_data = (struct clb_data_s *)user_data;
    unsigned int u, v, match = 1;
    const char *name = NULL, *rev = NULL;
    char *model_data = NULL;

    /* get and check the final name and revision of the schema to be retrieved */
    if (!mod_rev || !mod_rev[0]) {
        /* newest revision requested - get the newest revision from the list of available modules on server */
        match = 0;
        for (u = 0; clb_data->schemas[u].name; ++u) {
            if (strcmp(mod_name, clb_data->schemas[u].name)) {
                continue;
            }
            if (!match || strcmp(mod_rev, clb_data->schemas[u].revision) > 0) {
                mod_rev = clb_data->schemas[u].revision;
            }
            match = u + 1;
        }
        if (!match) {
            WRN("Session %u: unable to identify revision of the schema \"%s\" from the available server side information.",
                clb_data->session->id, mod_name);
        }
    }
    if (submod_name) {
        name = submod_name;
        if (sub_rev) {
            rev = sub_rev;
        } else if (match) {
            if (!clb_data->schemas[match - 1].submodules) {
                WRN("Session %u: Unable to identify revision of the requested submodule \"%s\", in schema \"%s\", from the available server side information.",
                    clb_data->session->id, submod_name, mod_name);
            } else {
                for (v = 0; clb_data->schemas[match - 1].submodules[v].name; ++v) {
                    if (!strcmp(submod_name, clb_data->schemas[match - 1].submodules[v].name)) {
                        rev = sub_rev = clb_data->schemas[match - 1].submodules[v].revision;
                    }
                }
                if (!rev) {
                    ERR("Session %u: requested submodule \"%s\" is not known for schema \"%s\" on server side.",
                        clb_data->session->id, submod_name, mod_name);
                    return NULL;
                }
            }
        }
    } else {
        name = mod_name;
        rev = mod_rev;
    }

    VRB("Session %u: retreiving data for schema \"%s\", revision \"%s\".", clb_data->session->id, name, rev);

    if (match) {
        /* we have enough information to avoid communication with server and try to get
         * the schema locally */

        /* 1. try to get data locally */
        model_data = retrieve_schema_data_localfile(name, rev, clb_data, format);

        /* 2. try to use <get-schema> */
        if (!model_data && clb_data->has_get_schema) {
            model_data = retrieve_schema_data_getschema(name, rev, clb_data, format);
        }
    } else {
        /* we are unsure which revision of the schema we should load, so first try to get
         * the newest revision from the server via get-schema and only if the server does not
         * implement get-schema, try to load the newest revision locally. This is imperfect
         * solution, but there are situation when a client does not know what revision is
         * actually implemented by the server. */

        /* 1. try to use <get-schema> */
        if (clb_data->has_get_schema) {
            model_data = retrieve_schema_data_getschema(name, rev, clb_data, format);
        }

        /* 2. try to get data locally */
        if (!model_data) {
            model_data = retrieve_schema_data_localfile(name, rev, clb_data, format);
        }
    }

    /* 3. try to use user callback */
    if (!model_data && clb_data->user_clb) {
        VRB("Session %u: reading schema via user callback.", clb_data->session->id);
        return clb_data->user_clb(mod_name, mod_rev, submod_name, sub_rev, clb_data->user_data, format, free_module_data);
    }

    *free_module_data = free_with_user_data;
    return model_data;
}

static int
nc_ctx_load_module(struct nc_session *session, const char *name, const char *revision, struct schema_info *schemas,
                   ly_module_imp_clb user_clb, void *user_data, int has_get_schema, const struct lys_module **mod)
{
    int ret = 0;
    struct ly_err_item *eitem;
    const char *module_data = NULL;
    LYS_INFORMAT format;
    void (*free_module_data)(void*, void*) = NULL;
    struct clb_data_s clb_data;

    *mod = NULL;
    if (revision) {
        *mod = ly_ctx_get_module(session->ctx, name, revision, 0);
    }
    if (*mod) {
        if (!(*mod)->implemented) {
            /* make the present module implemented */
            if (lys_set_implemented(*mod)) {
                ERR("Failed to implement model \"%s\".", (*mod)->name);
                ret = -1;
            }
        }
    } else {
        /* missing implemented module, load it ... */
        clb_data.has_get_schema = has_get_schema;
        clb_data.schemas = schemas;
        clb_data.session = session;
        clb_data.user_clb = user_clb;
        clb_data.user_data = user_data;

        /* clear all the errors and just collect them for now */
        ly_err_clean(session->ctx, NULL);
        ly_log_options(LY_LOSTORE);

        /* get module data */
        module_data = retrieve_schema_data(name, revision, NULL, NULL, &clb_data, &format, &free_module_data);

        if (module_data) {
            /* parse the schema */
            ly_ctx_set_module_imp_clb(session->ctx, retrieve_schema_data, &clb_data);

            *mod = lys_parse_mem(session->ctx, module_data, format);
            if (*free_module_data) {
                (*free_module_data)((char*)module_data, user_data);
            }

            ly_ctx_set_module_imp_clb(session->ctx, NULL, NULL);
        }

        /* restore logging options, then print errors on definite failure */
        ly_log_options(LY_LOLOG | LY_LOSTORE_LAST);
        if (!(*mod)) {
            for (eitem = ly_err_first(session->ctx); eitem && eitem->next; eitem = eitem->next) {
                ly_err_print(eitem);
            }
            ret = -1;
        } else {
            /* print only warnings */
            for (eitem = ly_err_first(session->ctx); eitem && eitem->next; eitem = eitem->next) {
                if (eitem->level == LY_LLWRN) {
                    ly_err_print(eitem);
                }
            }
        }

        /* clean the errors */
        ly_err_clean(session->ctx, NULL);
    }

    return ret;
}

static void
free_schema_info(struct schema_info *list)
{
    unsigned int u, v;

    if (!list) {
        return;
    }

    for (u = 0; list[u].name; ++u) {
        free(list[u].name);
        free(list[u].revision);
        for (v = 0; v < list[u].features.number; ++v) {
            free(list[u].features.set.g[v]);
        }
        free(list[u].features.set.g);
        if (list[u].submodules) {
            for (v = 0; list[u].submodules[v].name; ++v) {
                free(list[u].submodules[v].name);
                free(list[u].submodules[v].revision);
            }
            free(list[u].submodules);
        }
    }
    free(list);
}


static int
build_schema_info_yl(struct nc_session *session, struct schema_info **result)
{
    struct nc_rpc *rpc = NULL;
    struct nc_reply *reply = NULL;
    struct nc_reply_error *error_rpl;
    struct lyd_node *yldata = NULL;
    NC_MSG_TYPE msg;
    uint64_t msgid;
    struct ly_set *modules = NULL;
    unsigned int u, v, submodules_count;
    struct lyd_node *iter, *child;
    struct lys_module *mod;
    int ret = EXIT_SUCCESS;

    /* get yang-library data from the server */
    if (nc_session_cpblt(session, "urn:ietf:params:netconf:capability:xpath:1.0")) {
        rpc = nc_rpc_get("/ietf-yang-library:*", 0, NC_PARAMTYPE_CONST);
    } else {
        rpc = nc_rpc_get("<modules-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-yang-library\"/>", 0, NC_PARAMTYPE_CONST);
    }
    if (!rpc) {
        goto cleanup;
    }

    while ((msg = nc_send_rpc(session, rpc, 0, &msgid)) == NC_MSG_WOULDBLOCK) {
        usleep(1000);
    }
    if (msg == NC_MSG_ERROR) {
        WRN("Session %u: failed to send request for yang-library data.",
            session->id);
        goto cleanup;
    }

    do {
        msg = nc_recv_reply(session, rpc, msgid, NC_READ_ACT_TIMEOUT * 1000, 0, &reply);
    } while (msg == NC_MSG_NOTIF || msg == NC_MSG_REPLY_ERR_MSGID);
    if (msg == NC_MSG_WOULDBLOCK) {
        WRN("Session %u: timeout for receiving reply to a <get> yang-library data expired.", session->id);
        goto cleanup;
    } else if (msg == NC_MSG_ERROR || reply == NULL) {
        WRN("Session %u: failed to receive a reply to <get> of yang-library data.", session->id);
        goto cleanup;
    }

    switch (reply->type) {
    case NC_RPL_OK:
        WRN("Session %u: unexpected reply OK to a yang-library <get> RPC.", session->id);
        goto cleanup;
    case NC_RPL_DATA:
        /* fine */
        break;
    case NC_RPL_ERROR:
        error_rpl = (struct nc_reply_error *)reply;
        if (error_rpl->count) {
            WRN("Session %u: error reply to a yang-library <get> RPC (tag \"%s\", message \"%s\").",
                session->id, error_rpl->err[0].tag, error_rpl->err[0].message);
        } else {
            WRN("Session %u: unexpected reply error to a yang-library <get> RPC.", session->id);
        }
        goto cleanup;
    case NC_RPL_NOTIF:
        WRN("Session %u: unexpected reply notification to a yang-library <get> RPC.", session->id);
        goto cleanup;
    }

    yldata = ((struct nc_reply_data *)reply)->data;
    if (!yldata || strcmp(yldata->schema->module->name, "ietf-yang-library")) {
        WRN("Session %u: unexpected data in reply to a yang-library <get> RPC.", session->id);
        goto cleanup;
    }

    modules = lyd_find_path(yldata, "/ietf-yang-library:modules-state/module");
    if (!modules) {
        WRN("Session %u: no module information in reply to a yang-library <get> RPC.", session->id);
        goto cleanup;
    }

    (*result) = calloc(modules->number + 1, sizeof **result);
    if (!(*result)) {
        ERRMEM;
        ret = EXIT_FAILURE;
        goto cleanup;
    }

    for (u = 0; u < modules->number; ++u) {
        submodules_count = 0;
        mod = ((struct lyd_node *)modules->set.d[u])->schema->module;
        LY_TREE_FOR(modules->set.d[u]->child, iter) {
            if (iter->schema->module != mod) {
                /* ignore node from other schemas (augments) */
                continue;
            }
            if (!((struct lyd_node_leaf_list *)iter)->value_str || !((struct lyd_node_leaf_list *)iter)->value_str[0]) {
                /* ignore empty nodes */
                continue;
            }
            if (!strcmp(iter->schema->name, "name")) {
                (*result)[u].name = strdup(((struct lyd_node_leaf_list *)iter)->value_str);
            } else if (!strcmp(iter->schema->name, "revision")) {
                (*result)[u].revision = strdup(((struct lyd_node_leaf_list *)iter)->value_str);
            } else if (!strcmp(iter->schema->name, "conformance-type")) {
                (*result)[u].implemented = !strcmp(((struct lyd_node_leaf_list *)iter)->value_str, "implement");
            } else if (!strcmp(iter->schema->name, "feature")) {
                ly_set_add(&(*result)[u].features, (void *)strdup(((struct lyd_node_leaf_list *)iter)->value_str), LY_SET_OPT_USEASLIST);
            } else if (!strcmp(iter->schema->name, "submodule")) {
                submodules_count++;
            }
        }

        if (submodules_count) {
            (*result)[u].submodules = calloc(submodules_count + 1, sizeof *(*result)[u].submodules);
            if (!(*result)[u].submodules) {
                ERRMEM;
                free_schema_info(*result);
                *result = NULL;
                ret = EXIT_FAILURE;
                goto cleanup;
            } else {
                v = 0;
                LY_TREE_FOR(modules->set.d[u]->child, iter) {
                    mod = ((struct lyd_node *)modules->set.d[u])->schema->module;
                    if (mod == iter->schema->module && !strcmp(iter->schema->name, "submodule")) {
                        LY_TREE_FOR(iter->child, child) {
                            if (mod != child->schema->module) {
                                continue;
                            } else if (!strcmp(child->schema->name, "name")) {
                                (*result)[u].submodules[v].name = strdup(((struct lyd_node_leaf_list *)child)->value_str);
                            } else if (!strcmp(child->schema->name, "revision")) {
                                (*result)[u].submodules[v].name = strdup(((struct lyd_node_leaf_list *)child)->value_str);
                            }
                        }
                    }
                }
            }
        }
    }

cleanup:
    nc_rpc_free(rpc);
    nc_reply_free(reply);
    ly_set_free(modules);

    if (session->status != NC_STATUS_RUNNING) {
        /* something bad heppened, discard the session */
        ERR("Session %d: invalid session, discarding.", nc_session_get_id(session));
        ret = EXIT_FAILURE;
    }

    return ret;
}

static int
build_schema_info_cpblts(char **cpblts, struct schema_info **result)
{
    unsigned int u, v;
    char *module_cpblt, *ptr, *ptr2;

    for (u = 0; cpblts[u]; ++u);
    (*result) = calloc(u + 1, sizeof **result);
    if (!(*result)) {
        ERRMEM;
        return EXIT_FAILURE;
    }

    for (u = v = 0; cpblts[u]; ++u) {
        module_cpblt = strstr(cpblts[u], "module=");
        /* this capability requires a module */
        if (!module_cpblt) {
            continue;
        }

        /* get module's name */
        ptr = (char *)module_cpblt + 7;
        ptr2 = strchr(ptr, '&');
        if (!ptr2) {
            ptr2 = ptr + strlen(ptr);
        }
        (*result)[v].name = strndup(ptr, ptr2 - ptr);

        /* get module's revision */
        ptr = strstr(module_cpblt, "revision=");
        if (ptr) {
            ptr += 9;
            ptr2 = strchr(ptr, '&');
            if (!ptr2) {
                ptr2 = ptr + strlen(ptr);
            }
            (*result)[v].revision = strndup(ptr, ptr2 - ptr);
        }

        /* all are implemented since there is no better information in capabilities list */
        (*result)[v].implemented = 1;

        /* get module's features */
        ptr = strstr(module_cpblt, "features=");
        if (ptr) {
            ptr += 9;
            for (ptr2 = ptr; *ptr && *ptr != '&'; ++ptr) {
                if (*ptr == ',') {
                    ly_set_add(&(*result)[v].features, (void *)strndup(ptr2, ptr - ptr2), LY_SET_OPT_USEASLIST);
                    ptr2 = ptr + 1;
                }
            }
            /* the last one */
            ly_set_add(&(*result)[v].features, (void *)strndup(ptr2, ptr - ptr2), LY_SET_OPT_USEASLIST);
        }
        ++v;
    }

    return EXIT_SUCCESS;
}

static int
nc_ctx_fill(struct nc_session *session, struct schema_info *modules, ly_module_imp_clb user_clb, void *user_data, int has_get_schema)
{
    int ret = EXIT_FAILURE;
    const struct lys_module *mod;
    unsigned int u, v;

    for (u = 0; modules[u].name; ++u) {
        /* skip import-only modules */
        if (!modules[u].implemented) {
            continue;
        }

        /* we can continue even if it fails */
        nc_ctx_load_module(session, modules[u].name, modules[u].revision, modules, user_clb, user_data, has_get_schema, &mod);

        if (!mod) {
            if (session->status != NC_STATUS_RUNNING) {
                /* something bad heppened, discard the session */
                ERR("Session %d: invalid session, discarding.", nc_session_get_id(session));
                goto cleanup;
            }

            /* all loading ways failed, the schema will be ignored in the received data */
            WRN("Failed to load schema \"%s@%s\".", modules[u].name, modules[u].revision ? modules[u].revision : "<latest>");
            session->flags |= NC_SESSION_CLIENT_NOT_STRICT;
        } else {
            /* set features - first disable all to enable specified then */
            lys_features_disable(mod, "*");
            for (v = 0; v < modules[u].features.number; v++) {
                lys_features_enable(mod, (const char*)modules[u].features.set.g[v]);
            }
        }
    }

    /* done */
    ret = EXIT_SUCCESS;

cleanup:

    return ret;
}

static int
nc_ctx_fill_ietf_netconf(struct nc_session *session, struct schema_info *modules, ly_module_imp_clb user_clb, void *user_data, int has_get_schema)
{
    unsigned int u, v;
    const struct lys_module *ietfnc;

    ietfnc = ly_ctx_get_module(session->ctx, "ietf-netconf", NULL, 1);
    if (!ietfnc) {
        nc_ctx_load_module(session, "ietf-netconf", NULL, modules, user_clb, user_data, has_get_schema, &ietfnc);
        if (!ietfnc) {
            ietfnc = lys_parse_mem(session->ctx, ietf_netconf_2013_09_29_yang, LYS_YANG);
        }
    }
    if (!ietfnc) {
        ERR("Loading base NETCONF schema failed.");
        return 1;
    }

    /* set supported capabilities from ietf-netconf */
    for (u = 0; modules[u].name; ++u) {
        if (strcmp(modules[u].name, "ietf-netconf") || !modules[u].implemented) {
            continue;
        }

        lys_features_disable(ietfnc, "*");
        for (v = 0; v < modules[u].features.number; v++) {
            lys_features_enable(ietfnc, (const char*)modules[u].features.set.g[v]);
        }
    }

    return 0;
}

int
nc_ctx_check_and_fill(struct nc_session *session)
{
    int i, get_schema_support = 0, yanglib_support = 0, ret = -1;
    ly_module_imp_clb old_clb = NULL;
    void *old_data = NULL;
    const struct lys_module *mod = NULL;
    char *revision;
    struct schema_info *server_modules = NULL, *sm = NULL;

    assert(session->opts.client.cpblts && session->ctx);

    /* store the original user's callback, we will be switching between local search, get-schema and user callback */
    old_clb = ly_ctx_get_module_imp_clb(session->ctx, &old_data);

    /* switch off default searchpath to use only our callback integrating modifying searchpath algorithm to limit
     * schemas only to those present on the server side */
    ly_ctx_set_disable_searchdirs(session->ctx);

    /* our callback is set later with appropriate data */
    ly_ctx_set_module_imp_clb(session->ctx, NULL, NULL);

    /* check if get-schema and yang-library is supported */
    for (i = 0; session->opts.client.cpblts[i]; ++i) {
        if (!strncmp(session->opts.client.cpblts[i], "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring?", 52)) {
            get_schema_support = 1 + i;
            if (yanglib_support) {
                break;
            }
        } else if (!strncmp(session->opts.client.cpblts[i], "urn:ietf:params:netconf:capability:yang-library:", 48)) {
            yanglib_support = 1 + i;
            if (get_schema_support) {
                break;
            }
        }
    }
    if (get_schema_support) {
        VRB("Session %u: capability for <get-schema> support found.", session->id);
    } else {
        VRB("Session %u: capability for <get-schema> support not found.", session->id);
    }
    if (yanglib_support) {
        VRB("Session %u: capability for yang-library support found.", session->id);
    } else {
        VRB("Session %u: capability for yang-library support not found.", session->id);
    }

    /* get information about server's schemas from capabilities list until we will have yang-library */
    if (build_schema_info_cpblts(session->opts.client.cpblts, &server_modules) || !server_modules) {
        ERR("Session %u: unable to get server's schema information from the <hello>'s capabilities.", session->id);
        goto cleanup;
    }

    /* get-schema is supported, load local ietf-netconf-monitoring so we can create <get-schema> RPCs */
    if (get_schema_support && !lys_parse_mem(session->ctx, ietf_netconf_monitoring_2010_10_04_yang, LYS_YANG)) {
        WRN("Session %u: loading NETCONF monitoring schema failed, cannot use <get-schema>.", session->id);
        get_schema_support = 0;
    }

    /* load base model disregarding whether it's in capabilities (but NETCONF capabilities are used to enable features) */
    if (nc_ctx_fill_ietf_netconf(session, server_modules, old_clb, old_data, get_schema_support)) {
        goto cleanup;
    }

    /* get correct version of ietf-yang-library into context */
    if (yanglib_support) {
        /* use get schema to get server's ietf-yang-library */
        revision = strstr(session->opts.client.cpblts[yanglib_support - 1], "revision=");
        if (!revision) {
            WRN("Session %u: loading NETCONF ietf-yang-library schema failed, missing revision in NETCONF <hello> message.", session->id);
            WRN("Session %u: unable to automatically use <get-schema>.", session->id);
            yanglib_support = 0;
        } else {
            revision = strndup(&revision[9], 10);
            if (nc_ctx_load_module(session, "ietf-yang-library", revision, server_modules, old_clb, old_data,
                    get_schema_support, &mod)) {
                WRN("Session %u: loading NETCONF ietf-yang-library schema failed, unable to use it to learn all the supported modules.",
                    session->id);
                yanglib_support = 0;
            }
            if (strcmp(revision, "2019-01-04") >= 0) {
                /* we also need ietf-datastores to be implemented */
                if (nc_ctx_load_module(session, "ietf-datastores", NULL, server_modules, old_clb, old_data,
                        get_schema_support, &mod)) {
                    WRN("Session %u: loading NETCONF ietf-datastores schema failed, unable to use yang-library"
                        " to learn all the supported modules.", session->id);
                    yanglib_support = 0;
                }
            }
            free(revision);
        }
    }

    /* prepare structured information about server's schemas */
    if (yanglib_support) {
        if (build_schema_info_yl(session, &sm)) {
            goto cleanup;
        } else if (!sm) {
            VRB("Session %u: trying to use capabilities instead of ietf-yang-library data.", session->id);
        } else {
            /* prefer yang-library information, currently we have it from capabilities used for getting correct yang-library schema */
            free_schema_info(server_modules);
            server_modules = sm;
        }
    }

    if (nc_ctx_fill(session, server_modules, old_clb, old_data, get_schema_support)) {
        goto cleanup;
    }

    /* succsess */
    ret = 0;

    if (session->flags & NC_SESSION_CLIENT_NOT_STRICT) {
        WRN("Session %u: some models failed to be loaded, any data from these models (and any other unknown) will be ignored.", session->id);
    }

cleanup:
    free_schema_info(server_modules);

    /* set user callback back */
    ly_ctx_set_module_imp_clb(session->ctx, old_clb, old_data);
    ly_ctx_unset_disable_searchdirs(session->ctx);

    return ret;
}

API struct nc_session *
nc_connect_inout(int fdin, int fdout, struct ly_ctx *ctx)
{
    struct nc_session *session;

    if (fdin < 0) {
        ERRARG("fdin");
        return NULL;
    } else if (fdout < 0) {
        ERRARG("fdout");
        return NULL;
    }

    /* prepare session structure */
    session = nc_new_session(NC_CLIENT, 0);
    if (!session) {
        ERRMEM;
        return NULL;
    }
    session->status = NC_STATUS_STARTING;

    /* transport specific data */
    session->ti_type = NC_TI_FD;
    session->ti.fd.in = fdin;
    session->ti.fd.out = fdout;

    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
        goto fail;
    }
    ctx = session->ctx;

    /* NETCONF handshake */
    if (nc_handshake_io(session) != NC_MSG_HELLO) {
        goto fail;
    }
    session->status = NC_STATUS_RUNNING;

    if (nc_ctx_check_and_fill(session) == -1) {
        goto fail;
    }

    return session;

fail:
    nc_session_free(session, NULL);
    return NULL;
}

API struct nc_session *
nc_connect_unix(const char *address, struct ly_ctx *ctx)
{
    struct nc_session *session = NULL;
    struct sockaddr_un sun;
    const struct passwd *pw;
    char *username;
    int sock = -1;

    if (address == NULL) {
        ERRARG("address");
        return NULL;
    }

    pw = getpwuid(geteuid());
    if (pw == NULL) {
        ERR("Failed to find username for euid=%u.\n", geteuid());
        goto fail;
    }

    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock < 0) {
        ERR("Failed to create socket (%s).", strerror(errno));
        goto fail;
    }

    memset(&sun, 0, sizeof(sun));
    sun.sun_family = AF_UNIX;
    snprintf(sun.sun_path, sizeof(sun.sun_path), "%s", address);

    if (connect(sock, (struct sockaddr *)&sun, sizeof(sun)) < 0) {
        ERR("cannot connect to sock server %s (%s)",
            address, strerror(errno));
        goto fail;
    }

    if (fcntl(sock, F_SETFL, O_NONBLOCK) < 0) {
        ERR("Fcntl failed (%s).", strerror(errno));
        goto fail;
    }

    /* prepare session structure */
    session = nc_new_session(NC_CLIENT, 0);
    if (!session) {
        ERRMEM;
        goto fail;
    }
    session->status = NC_STATUS_STARTING;

    /* transport specific data */
    session->ti_type = NC_TI_UNIX;
    session->ti.unixsock.sock = sock;
    sock = -1; /* do not close sock in fail label anymore */

    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
        goto fail;
    }
    ctx = session->ctx;

    session->path = lydict_insert(ctx, address, 0);

    username = strdup(pw->pw_name);
    if (username == NULL) {
        ERRMEM;
        goto fail;
    }
    session->username = lydict_insert_zc(ctx, username);

    /* NETCONF handshake */
    if (nc_handshake_io(session) != NC_MSG_HELLO) {
        goto fail;
    }
    session->status = NC_STATUS_RUNNING;

    if (nc_ctx_check_and_fill(session) == -1) {
        goto fail;
    }

    return session;

fail:
    nc_session_free(session, NULL);
    if (sock >= 0)
        close(sock);
    return NULL;
}

/*
   Helper for a non-blocking connect (which is required because of the locking
   concept for e.g. call home settings). For more details see nc_sock_connect().
 */
static int
_non_blocking_connect(int timeout, int *sock_pending, struct addrinfo *res, struct nc_keepalives *ka)
{
    int flags, ret, error;
    int sock = -1;
    fd_set wset;
    struct timeval ts;
    socklen_t len = sizeof(int);
    struct in_addr *addr;
    uint16_t port;
    char str[INET6_ADDRSTRLEN];

    if (sock_pending && *sock_pending != -1) {
        VRB("Trying to connect the pending socket=%d.", *sock_pending );
        sock = *sock_pending;
    } else {
        assert(res);
        if (res->ai_family == AF_INET6) {
            addr = (struct in_addr *) &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr;
            port = ntohs(((struct sockaddr_in6 *)res->ai_addr)->sin6_port);
        } else {
            addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
            port = ntohs(((struct sockaddr_in *)res->ai_addr)->sin_port);
        }
        if (!inet_ntop(res->ai_family, addr, str, res->ai_addrlen)) {
            WRN("inet_ntop() failed (%s).", strerror(errno));
        } else {
            VRB("Trying to connect via %s to %s:%u.", (res->ai_family == AF_INET6) ? "IPv6" : "IPv4", str, port);
        }

        /* connect to a server */
        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (sock == -1) {
            ERR("Socket could not be created (%s).", strerror(errno));
            return -1;
        }
        /* make the socket non-blocking */
        if (((flags = fcntl(sock, F_GETFL)) == -1) || (fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1)) {
            ERR("fcntl() failed (%s).", strerror(errno));
            goto cleanup;
        }
        /* non-blocking connect! */
        if (connect(sock, res->ai_addr, res->ai_addrlen) < 0) {
            if (errno != EINPROGRESS) {
                /* network connection failed, try another resource */
                ERR("connect() failed (%s).", strerror(errno));
                goto cleanup;
            }
        }
    }
    ts.tv_sec = timeout;
    ts.tv_usec = 0;

    FD_ZERO(&wset);
    FD_SET(sock, &wset);

    if ((ret = select(sock + 1, NULL, &wset, NULL, (timeout != -1) ? &ts : NULL)) < 0) {
        ERR("select() failed (%s).", strerror(errno));
        goto cleanup;
    }

    if (ret == 0) {
        /* there was a timeout */
        VRB("Timed out after %ds (%s).", timeout, strerror(errno));
        if (sock_pending) {
            /* no sock-close, we'll try it again */
            *sock_pending = sock;
        } else {
            close(sock);
        }
        return -1;
    }

    /* check the usability of the socket */
    error = 0;
    if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
        ERR("getsockopt() failed (%s).", strerror(errno));
        goto cleanup;
    }
    if (error == ECONNREFUSED) {
        /* network connection failed, try another resource */
        VRB("getsockopt() error (%s).", strerror(error));
        errno = error;
        goto cleanup;
    }

    /* enable keep-alive */
    if (nc_sock_enable_keepalive(sock, ka)) {
        goto cleanup;
    }

    return sock;

cleanup:
    if (sock_pending) {
        *sock_pending = -1;
    }
    close(sock);
    return -1;
}

/* A given timeout value limits the time how long the function blocks. If it has to block
   only for some seconds, a socket connection might not yet have been fully established.
   Therefore the active (pending) socket will be stored in *sock_pending, but the return
   value will be -1. In such a case a subsequent invokation is required, by providing the
   stored sock_pending, again.
   In general, if this function returns -1, when a timeout has been given, this function
   has to be invoked, until it returns a valid socket.
 */
int
nc_sock_connect(const char *host, uint16_t port, int timeout, struct nc_keepalives *ka, int *sock_pending, char **ip_host)
{
    int i, opt;
    int sock = sock_pending ? *sock_pending : -1;
    struct addrinfo hints, *res_list, *res;
    char *buf, port_s[6]; /* length of string representation of short int */
    void *addr;

    DBG("nc_sock_connect(%s, %u, %d, %d)", host, port, timeout, sock);

    /* no pending socket */
    if (sock == -1) {
        /* connect to a server */
        snprintf(port_s, 6, "%u", port);
        memset(&hints, 0, sizeof hints);
        hints.ai_family = AF_UNSPEC;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;
        i = getaddrinfo(host, port_s, &hints, &res_list);
        if (i != 0) {
            ERR("Unable to translate the host address (%s).", gai_strerror(i));
            goto error;
        }

        for (res = res_list; res != NULL; res = res->ai_next) {
            sock = _non_blocking_connect(timeout, sock_pending, res, ka);
            if (sock == -1 && (!sock_pending || *sock_pending == -1)) {
                /* try the next resource */
                continue;
            }
            VRB("Successfully connected to %s:%s over %s.", host, port_s, (res->ai_family == AF_INET6) ? "IPv6" : "IPv4");

            opt = 1;
            if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof opt) == -1) {
                ERR("Could not set TCP_NODELAY socket option (%s).", strerror(errno));
                goto error;
            }

            if (ip_host && ((res->ai_family == AF_INET6) || (res->ai_family == AF_INET))) {
                buf = malloc(INET6_ADDRSTRLEN);
                if (!buf) {
                    ERRMEM;
                    goto error;
                }
                if (res->ai_family == AF_INET) {
                    addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
                } else {
                    addr = &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr;
                }
                if (!inet_ntop(res->ai_family, addr, buf, INET6_ADDRSTRLEN)) {
                    ERR("Converting host to IP address failed (%s).", strerror(errno));
                    free(buf);
                    goto error;
                }

                *ip_host = buf;
            }
            break;
        }
        freeaddrinfo(res_list);

    } else {
        /* try to get a connection with the pending socket */
        assert(sock_pending);
        sock = _non_blocking_connect(timeout, sock_pending, NULL, ka);
    }

    return sock;

error:
    if (sock != -1) {
        close(sock);
    }
    if (sock_pending) {
        *sock_pending = -1;
    }
    return -1;
}

static NC_MSG_TYPE
get_msg(struct nc_session *session, int timeout, uint64_t msgid, struct lyxml_elem **msg)
{
    char *ptr;
    const char *str_msgid;
    uint64_t cur_msgid;
    struct lyxml_elem *xml;
    struct nc_msg_cont *cont, **cont_ptr;
    NC_MSG_TYPE msgtype = 0; /* NC_MSG_ERROR */

    /* try to get notification from the session's queue */
    if (!msgid && session->opts.client.notifs) {
        cont = session->opts.client.notifs;
        session->opts.client.notifs = cont->next;

        xml = cont->msg;
        free(cont);

        msgtype = NC_MSG_NOTIF;
    }

    /* try to get rpc-reply from the session's queue */
    if (msgid && session->opts.client.replies) {
        cont = session->opts.client.replies;
        session->opts.client.replies = cont->next;

        xml = cont->msg;
        free(cont);

        msgtype = NC_MSG_REPLY;
    }

    if (!msgtype) {
        /* read message from wire */
        msgtype = nc_read_msg_poll_io(session, timeout, &xml);
    }

    /* we read rpc-reply, want a notif */
    if (!msgid && (msgtype == NC_MSG_REPLY)) {
        cont_ptr = &session->opts.client.replies;
        while (*cont_ptr) {
            cont_ptr = &((*cont_ptr)->next);
        }
        *cont_ptr = malloc(sizeof **cont_ptr);
        if (!*cont_ptr) {
            ERRMEM;
            lyxml_free(session->ctx, xml);
            return NC_MSG_ERROR;
        }
        (*cont_ptr)->msg = xml;
        (*cont_ptr)->next = NULL;
    }

    /* we read notif, want a rpc-reply */
    if (msgid && (msgtype == NC_MSG_NOTIF)) {
        if (!session->opts.client.ntf_tid) {
            ERR("Session %u: received a <notification> but session is not subscribed.", session->id);
            lyxml_free(session->ctx, xml);
            return NC_MSG_ERROR;
        }

        cont_ptr = &session->opts.client.notifs;
        while (*cont_ptr) {
            cont_ptr = &((*cont_ptr)->next);
        }
        *cont_ptr = malloc(sizeof **cont_ptr);
        if (!*cont_ptr) {
            ERRMEM;
            lyxml_free(session->ctx, xml);
            return NC_MSG_ERROR;
        }
        (*cont_ptr)->msg = xml;
        (*cont_ptr)->next = NULL;
    }

    switch (msgtype) {
    case NC_MSG_NOTIF:
        if (!msgid) {
            *msg = xml;
        }
        break;

    case NC_MSG_REPLY:
        if (msgid) {
            /* check message-id */
            str_msgid = lyxml_get_attr(xml, "message-id", NULL);
            if (!str_msgid) {
                WRN("Session %u: received a <rpc-reply> without a message-id.", session->id);
            } else {
                cur_msgid = strtoul(str_msgid, &ptr, 10);
                if (cur_msgid != msgid) {
                    ERR("Session %u: received a <rpc-reply> with an unexpected message-id \"%s\".",
                        session->id, str_msgid);
                    msgtype = NC_MSG_REPLY_ERR_MSGID;
                }
            }
            *msg = xml;
        }
        break;

    case NC_MSG_HELLO:
        ERR("Session %u: received another <hello> message.", session->id);
        lyxml_free(session->ctx, xml);
        msgtype = NC_MSG_ERROR;
        break;

    case NC_MSG_RPC:
        ERR("Session %u: received <rpc> from a NETCONF server.", session->id);
        lyxml_free(session->ctx, xml);
        msgtype = NC_MSG_ERROR;
        break;

    default:
        /* NC_MSG_WOULDBLOCK and NC_MSG_ERROR - pass it out;
         * NC_MSG_NONE is not returned by nc_read_msg()
         */
        break;
    }

    return msgtype;
}

/* cannot strictly fail, but does not need to fill any error parameter at all */
static void
parse_rpc_error(struct ly_ctx *ctx, struct lyxml_elem *xml, struct nc_err *err)
{
    struct lyxml_elem *iter, *next, *info;

    LY_TREE_FOR(xml->child, iter) {
        if (!iter->ns) {
            if (iter->content) {
                WRN("<rpc-error> child \"%s\" with value \"%s\" without namespace.", iter->name, iter->content);
            } else {
                WRN("<rpc-error> child \"%s\" without namespace.", iter->name);
            }
            continue;
        } else if (strcmp(iter->ns->value, NC_NS_BASE)) {
            if (iter->content) {
                WRN("<rpc-error> child \"%s\" with value \"%s\" in an unknown namespace \"%s\".",
                    iter->name, iter->content, iter->ns->value);
            } else {
                WRN("<rpc-error> child \"%s\" in an unknown namespace \"%s\".", iter->name, iter->ns->value);
            }
            continue;
        }

        if (!strcmp(iter->name, "error-type")) {
            if (!iter->content || (strcmp(iter->content, "transport") && strcmp(iter->content, "rpc")
                    && strcmp(iter->content, "protocol") && strcmp(iter->content, "application"))) {
                WRN("<rpc-error> <error-type> unknown value \"%s\".", (iter->content ? iter->content : ""));
            } else if (err->type) {
                WRN("<rpc-error> <error-type> duplicated.");
            } else {
                err->type = lydict_insert(ctx, iter->content, 0);
            }
        } else if (!strcmp(iter->name, "error-tag")) {
            if (!iter->content || (strcmp(iter->content, "in-use") && strcmp(iter->content, "invalid-value")
                    && strcmp(iter->content, "too-big") && strcmp(iter->content, "missing-attribute")
                    && strcmp(iter->content, "bad-attribute") && strcmp(iter->content, "unknown-attribute")
                    && strcmp(iter->content, "missing-element") && strcmp(iter->content, "bad-element")
                    && strcmp(iter->content, "unknown-element") && strcmp(iter->content, "unknown-namespace")
                    && strcmp(iter->content, "access-denied") && strcmp(iter->content, "lock-denied")
                    && strcmp(iter->content, "resource-denied") && strcmp(iter->content, "rollback-failed")
                    && strcmp(iter->content, "data-exists") && strcmp(iter->content, "data-missing")
                    && strcmp(iter->content, "operation-not-supported") && strcmp(iter->content, "operation-failed")
                    && strcmp(iter->content, "malformed-message"))) {
                WRN("<rpc-error> <error-tag> unknown value \"%s\".", (iter->content ? iter->content : ""));
            } else if (err->tag) {
                WRN("<rpc-error> <error-tag> duplicated.");
            } else {
                err->tag = lydict_insert(ctx, iter->content, 0);
            }
        } else if (!strcmp(iter->name, "error-severity")) {
            if (!iter->content || (strcmp(iter->content, "error") && strcmp(iter->content, "warning"))) {
                WRN("<rpc-error> <error-severity> unknown value \"%s\".", (iter->content ? iter->content : ""));
            } else if (err->severity) {
                WRN("<rpc-error> <error-severity> duplicated.");
            } else {
                err->severity = lydict_insert(ctx, iter->content, 0);
            }
        } else if (!strcmp(iter->name, "error-app-tag")) {
            if (err->apptag) {
                WRN("<rpc-error> <error-app-tag> duplicated.");
            } else {
                err->apptag = lydict_insert(ctx, (iter->content ? iter->content : ""), 0);
            }
        } else if (!strcmp(iter->name, "error-path")) {
            if (err->path) {
                WRN("<rpc-error> <error-path> duplicated.");
            } else {
                err->path = lydict_insert(ctx, (iter->content ? iter->content : ""), 0);
            }
        } else if (!strcmp(iter->name, "error-message")) {
            if (err->message) {
                WRN("<rpc-error> <error-message> duplicated.");
            } else {
                err->message_lang = lyxml_get_attr(iter, "xml:lang", NULL);
                if (err->message_lang) {
                    err->message_lang = lydict_insert(ctx, err->message_lang, 0);
                } else {
                    VRB("<rpc-error> <error-message> without the recommended \"xml:lang\" attribute.");
                }
                err->message = lydict_insert(ctx, (iter->content ? iter->content : ""), 0);
            }
        } else if (!strcmp(iter->name, "error-info")) {
            LY_TREE_FOR_SAFE(iter->child, next, info) {
                if (info->ns && !strcmp(info->ns->value, NC_NS_BASE)) {
                    if (!strcmp(info->name, "session-id")) {
                        if (err->sid) {
                            WRN("<rpc-error> <error-info> <session-id> duplicated.");
                        } else {
                            err->sid = lydict_insert(ctx, (info->content ? info->content : ""), 0);
                        }
                    } else if (!strcmp(info->name, "bad-attribute")) {
                        ++err->attr_count;
                        err->attr = nc_realloc(err->attr, err->attr_count * sizeof *err->attr);
                        if (!err->attr) {
                            ERRMEM;
                            return;
                        }
                        err->attr[err->attr_count - 1] = lydict_insert(ctx, (info->content ? info->content : ""), 0);
                    } else if (!strcmp(info->name, "bad-element")) {
                        ++err->elem_count;
                        err->elem = nc_realloc(err->elem, err->elem_count * sizeof *err->elem);
                        if (!err->elem) {
                            ERRMEM;
                            return;
                        }
                        err->elem[err->elem_count - 1] = lydict_insert(ctx, (info->content ? info->content : ""), 0);
                    } else if (!strcmp(info->name, "bad-namespace")) {
                        ++err->ns_count;
                        err->ns = nc_realloc(err->ns, err->ns_count * sizeof *err->ns);
                        if (!err->ns) {
                            ERRMEM;
                            return;
                        }
                        err->ns[err->ns_count - 1] = lydict_insert(ctx, (info->content ? info->content : ""), 0);
                    } else {
                        if (info->content) {
                            WRN("<rpc-error> <error-info> unknown child \"%s\" with value \"%s\".",
                                info->name, info->content);
                        } else {
                            WRN("<rpc-error> <error-info> unknown child \"%s\".", info->name);
                        }
                    }
                } else {
                    lyxml_unlink(ctx, info);
                    ++err->other_count;
                    err->other = nc_realloc(err->other, err->other_count * sizeof *err->other);
                    if (!err->other) {
                        ERRMEM;
                        return;
                    }
                    err->other[err->other_count - 1] = info;
                }
            }
        } else {
            if (iter->content) {
                WRN("<rpc-error> unknown child \"%s\" with value \"%s\".", iter->name, iter->content);
            } else {
                WRN("<rpc-error> unknown child \"%s\".", iter->name);
            }
        }
    }
}

static struct nc_reply *
parse_reply(struct ly_ctx *ctx, struct lyxml_elem *xml, struct nc_rpc *rpc, int parseroptions)
{
    struct lyxml_elem *iter;
    struct lyd_node *data = NULL, *rpc_act = NULL;
    struct nc_client_reply_error *error_rpl;
    struct nc_reply_data *data_rpl;
    struct nc_reply *reply = NULL;
    struct nc_rpc_act_generic *rpc_gen;
    int i, data_parsed = 0;

    /* rpc-error */
    if (xml->child && !strcmp(xml->child->name, "rpc-error") && xml->child->ns && !strcmp(xml->child->ns->value, NC_NS_BASE)) {
        /* count and check elements */
        i = 0;
        LY_TREE_FOR(xml->child, iter) {
            if (strcmp(iter->name, "rpc-error")) {
                ERR("<rpc-reply> content mismatch (<rpc-error> and <%s>).", iter->name);
                return NULL;
            } else if (!iter->ns) {
                ERR("<rpc-reply> content mismatch (<rpc-error> without namespace).");
                return NULL;
            } else if (strcmp(iter->ns->value, NC_NS_BASE)) {
                ERR("<rpc-reply> content mismatch (<rpc-error> with NS \"%s\").", iter->ns->value);
                return NULL;
            }
            ++i;
        }

        error_rpl = malloc(sizeof *error_rpl);
        if (!error_rpl) {
            ERRMEM;
            return NULL;
        }
        error_rpl->type = NC_RPL_ERROR;
        error_rpl->err = calloc(i, sizeof *error_rpl->err);
        if (!error_rpl->err) {
            ERRMEM;
            free(error_rpl);
            return NULL;
        }
        error_rpl->count = i;
        error_rpl->ctx = ctx;
        reply = (struct nc_reply *)error_rpl;

        i = 0;
        LY_TREE_FOR(xml->child, iter) {
            parse_rpc_error(ctx, iter, error_rpl->err + i);
            ++i;
        }

    /* ok */
    } else if (xml->child && !strcmp(xml->child->name, "ok") && xml->child->ns && !strcmp(xml->child->ns->value, NC_NS_BASE)) {
        if (xml->child->next) {
            ERR("<rpc-reply> content mismatch (<ok> and <%s>).", xml->child->next->name);
            return NULL;
        }
        reply = malloc(sizeof *reply);
        if (!reply) {
            ERRMEM;
            return NULL;
        }
        reply->type = NC_RPL_OK;

    /* some RPC output */
    } else {
        switch (rpc->type) {
        case NC_RPC_ACT_GENERIC:
            rpc_gen = (struct nc_rpc_act_generic *)rpc;

            if (rpc_gen->has_data) {
                rpc_act = lyd_dup(rpc_gen->content.data, 1);
                if (!rpc_act) {
                    ERR("Failed to duplicate a generic RPC/action.");
                    return NULL;
                }
            } else {
                rpc_act = lyd_parse_mem(ctx, rpc_gen->content.xml_str, LYD_XML, LYD_OPT_RPC | parseroptions, NULL);
                if (!rpc_act) {
                    ERR("Failed to parse a generic RPC/action XML.");
                    return NULL;
                }
            }
            break;

        case NC_RPC_GETCONFIG:
        case NC_RPC_GET:
        case NC_RPC_GETDATA:
            /* we should definitely have received at least an empty "data" element even on empty reply, but fine */
            if (!xml->child || !xml->child->child) {
                /* we did not receive any data */
                data_rpl = malloc(sizeof *data_rpl);
                if (!data_rpl) {
                    ERRMEM;
                    return NULL;
                }
                data_rpl->type = NC_RPL_DATA;
                data_rpl->data = NULL;
                return (struct nc_reply *)data_rpl;
            }

            /* special treatment */
            ly_errno = 0;
            data = lyd_parse_xml(ctx, &xml->child->child,
                                 LYD_OPT_DESTRUCT | (rpc->type == NC_RPC_GETCONFIG ? LYD_OPT_GETCONFIG : LYD_OPT_GET)
                                 | parseroptions);
            if (ly_errno) {
                ERR("Failed to parse <%s> reply.", (rpc->type == NC_RPC_GETCONFIG ? "get-config" : "get"));
                return NULL;
            }
            data_parsed = 1;
            break;

        case NC_RPC_GETSCHEMA:
            rpc_act = lyd_new(NULL, ly_ctx_get_module(ctx, "ietf-netconf-monitoring", NULL, 1), "get-schema");
            if (!rpc_act) {
                ERRINT;
                return NULL;
            }
            break;

        case NC_RPC_EDIT:
        case NC_RPC_COPY:
        case NC_RPC_DELETE:
        case NC_RPC_LOCK:
        case NC_RPC_UNLOCK:
        case NC_RPC_KILL:
        case NC_RPC_COMMIT:
        case NC_RPC_DISCARD:
        case NC_RPC_CANCEL:
        case NC_RPC_VALIDATE:
        case NC_RPC_SUBSCRIBE:
        case NC_RPC_EDITDATA:
            /* there is no output defined */
            ERR("Unexpected data reply (root elem \"%s\").", xml->child ? xml->child->name : NULL);
            return NULL;
        default:
            ERRINT;
            return NULL;
        }

        data_rpl = malloc(sizeof *data_rpl);
        if (!data_rpl) {
            ERRMEM;
            return NULL;
        }
        data_rpl->type = NC_RPL_DATA;

        ly_errno = 0;
        if (!data_parsed) {
            data_rpl->data = lyd_parse_xml(ctx, &xml->child, LYD_OPT_RPCREPLY | LYD_OPT_DESTRUCT | parseroptions,
                                           rpc_act, NULL);
            if (!ly_errno && !data_rpl->data->child) {
                ERR("An empty data <rpc-reply>.");
                lyd_free_withsiblings(rpc_act);
                lyd_free(data_rpl->data);
                free(data_rpl);
                return NULL;
            }
        } else {
            /* <get>, <get-config>, <get-data> */
            data_rpl->data = data;
        }
        lyd_free_withsiblings(rpc_act);
        if (ly_errno) {
            ERR("Failed to parse <rpc-reply>.");
            free(data_rpl);
            return NULL;
        }
        reply = (struct nc_reply *)data_rpl;
    }

    return reply;
}

#if defined(NC_ENABLED_SSH) || defined(NC_ENABLED_TLS)

int
nc_client_ch_add_bind_listen(const char *address, uint16_t port, NC_TRANSPORT_IMPL ti)
{
    int sock;

    if (!address) {
        ERRARG("address");
        return -1;
    } else if (!port) {
        ERRARG("port");
        return -1;
    }

    sock = nc_sock_listen_inet(address, port, &client_opts.ka);
    if (sock == -1) {
        return -1;
    }

    ++client_opts.ch_bind_count;
    client_opts.ch_binds = nc_realloc(client_opts.ch_binds, client_opts.ch_bind_count * sizeof *client_opts.ch_binds);
    if (!client_opts.ch_binds) {
        ERRMEM;
        close(sock);
        return -1;
    }

    client_opts.ch_bind_ti = nc_realloc(client_opts.ch_bind_ti, client_opts.ch_bind_count * sizeof *client_opts.ch_bind_ti);
    if (!client_opts.ch_bind_ti) {
        ERRMEM;
        close(sock);
        return -1;
    }
    client_opts.ch_bind_ti[client_opts.ch_bind_count - 1] = ti;

    client_opts.ch_binds[client_opts.ch_bind_count - 1].address = strdup(address);
    if (!client_opts.ch_binds[client_opts.ch_bind_count - 1].address) {
        ERRMEM;
        close(sock);
        return -1;
    }
    client_opts.ch_binds[client_opts.ch_bind_count - 1].port = port;
    client_opts.ch_binds[client_opts.ch_bind_count - 1].sock = sock;
    client_opts.ch_binds[client_opts.ch_bind_count - 1].pollin = 0;

    return 0;
}

int
nc_client_ch_del_bind(const char *address, uint16_t port, NC_TRANSPORT_IMPL ti)
{
    uint32_t i;
    int ret = -1;

    if (!address && !port && !ti) {
        for (i = 0; i < client_opts.ch_bind_count; ++i) {
            close(client_opts.ch_binds[i].sock);
            free((char *)client_opts.ch_binds[i].address);

            ret = 0;
        }
        free(client_opts.ch_binds);
        client_opts.ch_binds = NULL;
        client_opts.ch_bind_count = 0;
    } else {
        for (i = 0; i < client_opts.ch_bind_count; ++i) {
            if ((!address || !strcmp(client_opts.ch_binds[i].address, address))
                    && (!port || (client_opts.ch_binds[i].port == port))
                    && (!ti || (client_opts.ch_bind_ti[i] == ti))) {
                close(client_opts.ch_binds[i].sock);
                free((char *)client_opts.ch_binds[i].address);

                --client_opts.ch_bind_count;
                if (!client_opts.ch_bind_count) {
                    free(client_opts.ch_binds);
                    client_opts.ch_binds = NULL;
                } else if (i < client_opts.ch_bind_count) {
                    memcpy(&client_opts.ch_binds[i], &client_opts.ch_binds[client_opts.ch_bind_count], sizeof *client_opts.ch_binds);
                    client_opts.ch_bind_ti[i] = client_opts.ch_bind_ti[client_opts.ch_bind_count];
                }

                ret = 0;
            }
        }
    }

    return ret;
}

API int
nc_accept_callhome(int timeout, struct ly_ctx *ctx, struct nc_session **session)
{
    int sock;
    char *host = NULL;
    uint16_t port, idx;

    if (!client_opts.ch_binds) {
        ERRINIT;
        return -1;
    } else if (!session) {
        ERRARG("session");
        return -1;
    }

    sock = nc_sock_accept_binds(client_opts.ch_binds, client_opts.ch_bind_count, timeout, &host, &port, &idx);

    if (sock < 1) {
        free(host);
        return sock;
    }

#ifdef NC_ENABLED_SSH
    if (client_opts.ch_bind_ti[idx] == NC_TI_LIBSSH) {
        *session = nc_accept_callhome_ssh_sock(sock, host, port, ctx, NC_TRANSPORT_TIMEOUT);
    } else
#endif
#ifdef NC_ENABLED_TLS
    if (client_opts.ch_bind_ti[idx] == NC_TI_OPENSSL) {
        *session = nc_accept_callhome_tls_sock(sock, host, port, ctx, NC_TRANSPORT_TIMEOUT);
    } else
#endif
    {
        close(sock);
        *session = NULL;
    }

    free(host);

    if (!(*session)) {
        return -1;
    }

    return 1;
}

#endif /* NC_ENABLED_SSH || NC_ENABLED_TLS */

API const char * const *
nc_session_get_cpblts(const struct nc_session *session)
{
    if (!session) {
        ERRARG("session");
        return NULL;
    }

    return (const char * const *)session->opts.client.cpblts;
}

API const char *
nc_session_cpblt(const struct nc_session *session, const char *capab)
{
    int i, len;

    if (!session) {
        ERRARG("session");
        return NULL;
    } else if (!capab) {
        ERRARG("capab");
        return NULL;
    }

    len = strlen(capab);
    for (i = 0; session->opts.client.cpblts[i]; ++i) {
        if (!strncmp(session->opts.client.cpblts[i], capab, len)) {
            return session->opts.client.cpblts[i];
        }
    }

    return NULL;
}

API int
nc_session_ntf_thread_running(const struct nc_session *session)
{
    if (!session || (session->side != NC_CLIENT)) {
        ERRARG("session");
        return 0;
    }

    return session->opts.client.ntf_tid ? 1 : 0;
}

API void
nc_client_init(void)
{
    nc_init();
}

API void
nc_client_destroy(void)
{
    nc_client_set_schema_searchpath(NULL);
#if defined(NC_ENABLED_SSH) || defined(NC_ENABLED_TLS)
    nc_client_ch_del_bind(NULL, 0, 0);
#endif
#ifdef NC_ENABLED_SSH
    nc_client_ssh_destroy_opts();
#endif
#ifdef NC_ENABLED_TLS
    nc_client_tls_destroy_opts();
#endif
    nc_destroy();
}

API NC_MSG_TYPE
nc_recv_reply(struct nc_session *session, struct nc_rpc *rpc, uint64_t msgid, int timeout, int parseroptions, struct nc_reply **reply)
{
    struct lyxml_elem *xml = NULL;
    NC_MSG_TYPE msgtype = 0; /* NC_MSG_ERROR */

    if (!session) {
        ERRARG("session");
        return NC_MSG_ERROR;
    } else if (!rpc) {
        ERRARG("rpc");
        return NC_MSG_ERROR;
    } else if (!msgid) {
        ERRARG("msgid");
        return NC_MSG_ERROR;
    } else if (!reply) {
        ERRARG("reply");
        return NC_MSG_ERROR;
    } else if (parseroptions & LYD_OPT_TYPEMASK) {
        ERRARG("parseroptions");
        return NC_MSG_ERROR;
    } else if ((session->status != NC_STATUS_RUNNING) || (session->side != NC_CLIENT)) {
        ERR("Session %u: invalid session to receive RPC replies.", session->id);
        return NC_MSG_ERROR;
    }
    parseroptions &= ~(LYD_OPT_DESTRUCT | LYD_OPT_NOSIBLINGS);
    if (!(session->flags & NC_SESSION_CLIENT_NOT_STRICT)) {
        parseroptions &= LYD_OPT_STRICT;
    }
    /* no mechanism to check external dependencies is provided */
    parseroptions|= LYD_OPT_NOEXTDEPS;
    *reply = NULL;

    msgtype = get_msg(session, timeout, msgid, &xml);

    if ((msgtype == NC_MSG_REPLY) || (msgtype == NC_MSG_REPLY_ERR_MSGID)) {
        *reply = parse_reply(session->ctx, xml, rpc, parseroptions);
        lyxml_free_withsiblings(session->ctx, xml);
        xml = NULL;
        if (!(*reply)) {
            return NC_MSG_ERROR;
        }
    }
    assert(!xml);

    return msgtype;
}

API NC_MSG_TYPE
nc_recv_notif(struct nc_session *session, int timeout, struct nc_notif **notif)
{
    struct lyxml_elem *xml, *ev_time;
    NC_MSG_TYPE msgtype = 0; /* NC_MSG_ERROR */

    if (!session) {
        ERRARG("session");
        return NC_MSG_ERROR;
    } else if (!notif) {
        ERRARG("notif");
        return NC_MSG_ERROR;
    } else if (session->status != NC_STATUS_RUNNING || session->side != NC_CLIENT) {
        ERR("Session %u: invalid session to receive Notifications.", session->id);
        return NC_MSG_ERROR;
    }

    msgtype = get_msg(session, timeout, 0, &xml);

    if (msgtype == NC_MSG_NOTIF) {
        *notif = calloc(1, sizeof **notif);
        if (!*notif) {
            ERRMEM;
            lyxml_free(session->ctx, xml);
            return NC_MSG_ERROR;
        }

        /* eventTime */
        LY_TREE_FOR(xml->child, ev_time) {
            if (!strcmp(ev_time->name, "eventTime")) {
                (*notif)->datetime = lydict_insert(session->ctx, ev_time->content, 0);
                /* lyd_parse does not know this element */
                lyxml_free(session->ctx, ev_time);
                break;
            }
        }
        if (!(*notif)->datetime) {
            ERR("Session %u: notification is missing the \"eventTime\" element.", session->id);
            goto fail;
        }

        /* notification body */
        (*notif)->tree = lyd_parse_xml(session->ctx, &xml->child, LYD_OPT_NOTIF | LYD_OPT_DESTRUCT | LYD_OPT_NOEXTDEPS
                                       | (session->flags & NC_SESSION_CLIENT_NOT_STRICT ? 0 : LYD_OPT_STRICT), NULL);
        lyxml_free(session->ctx, xml);
        xml = NULL;
        if (!(*notif)->tree) {
            ERR("Session %u: failed to parse a new notification.", session->id);
            goto fail;
        }
    }

    return msgtype;

fail:
    lydict_remove(session->ctx, (*notif)->datetime);
    lyd_free((*notif)->tree);
    free(*notif);
    *notif = NULL;
    lyxml_free(session->ctx, xml);

    return NC_MSG_ERROR;
}

static void *
nc_recv_notif_thread(void *arg)
{
    struct nc_ntf_thread_arg *ntarg;
    struct nc_session *session;
    void (*notif_clb)(struct nc_session *session, const struct nc_notif *notif);
    struct nc_notif *notif;
    NC_MSG_TYPE msgtype;
    pthread_t *ntf_tid;

    pthread_detach(pthread_self());

    ntarg = (struct nc_ntf_thread_arg *)arg;
    session = ntarg->session;
    notif_clb = ntarg->notif_clb;
    free(ntarg);

    /* remember our allocated tid, we will be freeing it */
    ntf_tid = (pthread_t *)session->opts.client.ntf_tid;

    while (session->opts.client.ntf_tid) {
        msgtype = nc_recv_notif(session, NC_CLIENT_NOTIF_THREAD_SLEEP / 1000, &notif);
        if (msgtype == NC_MSG_NOTIF) {
            notif_clb(session, notif);
            if (!strcmp(notif->tree->schema->name, "notificationComplete")
                    && !strcmp(notif->tree->schema->module->name, "nc-notifications")) {
                nc_notif_free(notif);
                break;
            }
            nc_notif_free(notif);
        } else if ((msgtype == NC_MSG_ERROR) && (session->status != NC_STATUS_RUNNING)) {
            /* quit this thread once the session is broken */
            break;
        }

        usleep(NC_CLIENT_NOTIF_THREAD_SLEEP);
    }

    VRB("Session %u: notification thread exit.", session->id);
    session->opts.client.ntf_tid = NULL;
    free(ntf_tid);
    return NULL;
}

API int
nc_recv_notif_dispatch(struct nc_session *session, void (*notif_clb)(struct nc_session *session, const struct nc_notif *notif))
{
    struct nc_ntf_thread_arg *ntarg;
    int ret;

    if (!session) {
        ERRARG("session");
        return -1;
    } else if (!notif_clb) {
        ERRARG("notif_clb");
        return -1;
    } else if ((session->status != NC_STATUS_RUNNING) || (session->side != NC_CLIENT)) {
        ERR("Session %u: invalid session to receive Notifications.", session->id);
        return -1;
    } else if (session->opts.client.ntf_tid) {
        ERR("Session %u: separate notification thread is already running.", session->id);
        return -1;
    }

    ntarg = malloc(sizeof *ntarg);
    if (!ntarg) {
        ERRMEM;
        return -1;
    }
    ntarg->session = session;
    ntarg->notif_clb = notif_clb;

    /* just so that nc_recv_notif_thread() does not immediately exit, the value does not matter */
    session->opts.client.ntf_tid = malloc(sizeof *session->opts.client.ntf_tid);
    if (!session->opts.client.ntf_tid) {
        ERRMEM;
        free(ntarg);
        return -1;
    }

    ret = pthread_create((pthread_t *)session->opts.client.ntf_tid, NULL, nc_recv_notif_thread, ntarg);
    if (ret) {
        ERR("Session %u: failed to create a new thread (%s).", strerror(errno));
        free(ntarg);
        free((pthread_t *)session->opts.client.ntf_tid);
        session->opts.client.ntf_tid = NULL;
        return -1;
    }

    return 0;
}

API NC_MSG_TYPE
nc_send_rpc(struct nc_session *session, struct nc_rpc *rpc, int timeout, uint64_t *msgid)
{
    NC_MSG_TYPE r;
    int dofree = 1;
    struct nc_rpc_act_generic *rpc_gen;
    struct nc_rpc_getconfig *rpc_gc;
    struct nc_rpc_edit *rpc_e;
    struct nc_rpc_copy *rpc_cp;
    struct nc_rpc_delete *rpc_del;
    struct nc_rpc_lock *rpc_lock;
    struct nc_rpc_get *rpc_g;
    struct nc_rpc_kill *rpc_k;
    struct nc_rpc_commit *rpc_com;
    struct nc_rpc_cancel *rpc_can;
    struct nc_rpc_validate *rpc_val;
    struct nc_rpc_getschema *rpc_gs;
    struct nc_rpc_subscribe *rpc_sub;
    struct nc_rpc_getdata *rpc_getd;
    struct nc_rpc_editdata *rpc_editd;
    struct lyd_node *data, *node;
    const struct lys_module *mod = NULL, *ietfncwd;
    int i;
    char str[11];
    uint64_t cur_msgid;

    if (!session) {
        ERRARG("session");
        return NC_MSG_ERROR;
    } else if (!rpc) {
        ERRARG("rpc");
        return NC_MSG_ERROR;
    } else if (!msgid) {
        ERRARG("msgid");
        return NC_MSG_ERROR;
    } else if (session->status != NC_STATUS_RUNNING || session->side != NC_CLIENT) {
        ERR("Session %u: invalid session to send RPCs.", session->id);
        return NC_MSG_ERROR;
    }

    switch (rpc->type) {
    case NC_RPC_ACT_GENERIC:
        /* checked when parsing */
        break;
    case NC_RPC_GETCONFIG:
    case NC_RPC_EDIT:
    case NC_RPC_COPY:
    case NC_RPC_DELETE:
    case NC_RPC_LOCK:
    case NC_RPC_UNLOCK:
    case NC_RPC_GET:
    case NC_RPC_KILL:
    case NC_RPC_COMMIT:
    case NC_RPC_DISCARD:
    case NC_RPC_CANCEL:
    case NC_RPC_VALIDATE:
        mod = ly_ctx_get_module(session->ctx, "ietf-netconf", NULL, 1);
        if (!mod) {
            ERR("Session %u: missing \"ietf-netconf\" schema in the context.", session->id);
            return NC_MSG_ERROR;
        }
        break;
    case NC_RPC_GETSCHEMA:
        mod = ly_ctx_get_module(session->ctx, "ietf-netconf-monitoring", NULL, 1);
        if (!mod) {
            ERR("Session %u: missing \"ietf-netconf-monitoring\" schema in the context.", session->id);
            return NC_MSG_ERROR;
        }
        break;
    case NC_RPC_SUBSCRIBE:
        mod = ly_ctx_get_module(session->ctx, "notifications", NULL, 1);
        if (!mod) {
            ERR("Session %u: missing \"notifications\" schema in the context.", session->id);
            return NC_MSG_ERROR;
        }
        break;
    case NC_RPC_GETDATA:
    case NC_RPC_EDITDATA:
        mod = ly_ctx_get_module(session->ctx, "ietf-netconf-nmda", NULL, 1);
        if (!mod) {
            ERR("Session %u: missing \"ietf-netconf-nmda\" schema in the context.", session->id);
            return NC_MSG_ERROR;
        }
        break;
    case NC_RPC_UNKNOWN:
        ERRINT;
        return NC_MSG_ERROR;
    }

    switch (rpc->type) {
    case NC_RPC_ACT_GENERIC:
        rpc_gen = (struct nc_rpc_act_generic *)rpc;

        if (rpc_gen->has_data) {
            data = rpc_gen->content.data;
            dofree = 0;
        } else {
            data = lyd_parse_mem(session->ctx, rpc_gen->content.xml_str, LYD_XML, LYD_OPT_RPC | LYD_OPT_NOEXTDEPS
                                 | (session->flags & NC_SESSION_CLIENT_NOT_STRICT ? 0 : LYD_OPT_STRICT), NULL);
            if (!data) {
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_GETCONFIG:
        rpc_gc = (struct nc_rpc_getconfig *)rpc;

        data = lyd_new(NULL, mod, "get-config");
        node = lyd_new(data, mod, "source");
        node = lyd_new_leaf(node, mod, ncds2str[rpc_gc->source], NULL);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        if (rpc_gc->filter) {
            if (!rpc_gc->filter[0] || (rpc_gc->filter[0] == '<')) {
                node = lyd_new_anydata(data, mod, "filter", rpc_gc->filter, LYD_ANYDATA_SXML);
                lyd_insert_attr(node, NULL, "type", "subtree");
            } else {
                node = lyd_new_anydata(data, mod, "filter", NULL, LYD_ANYDATA_CONSTSTRING);
                lyd_insert_attr(node, NULL, "type", "xpath");
                lyd_insert_attr(node, NULL, "select", rpc_gc->filter);
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_gc->wd_mode) {
            ietfncwd = ly_ctx_get_module(session->ctx, "ietf-netconf-with-defaults", NULL, 1);
            if (!ietfncwd) {
                ERR("Session %u: missing \"ietf-netconf-with-defaults\" schema in the context.", session->id);
                lyd_free(data);
                return NC_MSG_ERROR;
            }
            switch (rpc_gc->wd_mode) {
            case NC_WD_UNKNOWN:
                /* cannot get here */
                break;
            case NC_WD_ALL:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "report-all");
                break;
            case NC_WD_ALL_TAG:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "report-all-tagged");
                break;
            case NC_WD_TRIM:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "trim");
                break;
            case NC_WD_EXPLICIT:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "explicit");
                break;
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_EDIT:
        rpc_e = (struct nc_rpc_edit *)rpc;

        data = lyd_new(NULL, mod, "edit-config");
        node = lyd_new(data, mod, "target");
        node = lyd_new_leaf(node, mod, ncds2str[rpc_e->target], NULL);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }

        if (rpc_e->default_op) {
            node = lyd_new_leaf(data, mod, "default-operation", rpcedit_dfltop2str[rpc_e->default_op]);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_e->test_opt) {
            node = lyd_new_leaf(data, mod, "test-option", rpcedit_testopt2str[rpc_e->test_opt]);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_e->error_opt) {
            node = lyd_new_leaf(data, mod, "error-option", rpcedit_erropt2str[rpc_e->error_opt]);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (!rpc_e->edit_cont[0] || (rpc_e->edit_cont[0] == '<')) {
            node = lyd_new_anydata(data, mod, "config", rpc_e->edit_cont, LYD_ANYDATA_SXML);
        } else {
            node = lyd_new_leaf(data, mod, "url", rpc_e->edit_cont);
        }
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        break;

    case NC_RPC_COPY:
        rpc_cp = (struct nc_rpc_copy *)rpc;

        data = lyd_new(NULL, mod, "copy-config");
        node = lyd_new(data, mod, "target");
        if (rpc_cp->url_trg) {
            node = lyd_new_leaf(node, mod, "url", rpc_cp->url_trg);
        } else {
            node = lyd_new_leaf(node, mod, ncds2str[rpc_cp->target], NULL);
        }
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }

        node = lyd_new(data, mod, "source");
        if (rpc_cp->url_config_src) {
            if (!rpc_cp->url_config_src[0] || (rpc_cp->url_config_src[0] == '<')) {
                node = lyd_new_anydata(node, mod, "config", rpc_cp->url_config_src, LYD_ANYDATA_SXML);
            } else {
                node = lyd_new_leaf(node, mod, "url", rpc_cp->url_config_src);
            }
        } else {
            node = lyd_new_leaf(node, mod, ncds2str[rpc_cp->source], NULL);
        }
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }

        if (rpc_cp->wd_mode) {
            ietfncwd = ly_ctx_get_module(session->ctx, "ietf-netconf-with-defaults", NULL, 1);
            if (!ietfncwd) {
                ERR("Session %u: missing \"ietf-netconf-with-defaults\" schema in the context.", session->id);
                lyd_free(data);
                return NC_MSG_ERROR;
            }
            switch (rpc_cp->wd_mode) {
            case NC_WD_UNKNOWN:
                /* cannot get here */
                break;
            case NC_WD_ALL:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "report-all");
                break;
            case NC_WD_ALL_TAG:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "report-all-tagged");
                break;
            case NC_WD_TRIM:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "trim");
                break;
            case NC_WD_EXPLICIT:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "explicit");
                break;
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_DELETE:
        rpc_del = (struct nc_rpc_delete *)rpc;

        data = lyd_new(NULL, mod, "delete-config");
        node = lyd_new(data, mod, "target");
        if (rpc_del->url) {
            node = lyd_new_leaf(node, mod, "url", rpc_del->url);
        } else {
            node = lyd_new_leaf(node, mod, ncds2str[rpc_del->target], NULL);
        }
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        break;

    case NC_RPC_LOCK:
        rpc_lock = (struct nc_rpc_lock *)rpc;

        data = lyd_new(NULL, mod, "lock");
        node = lyd_new(data, mod, "target");
        node = lyd_new_leaf(node, mod, ncds2str[rpc_lock->target], NULL);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        break;

    case NC_RPC_UNLOCK:
        rpc_lock = (struct nc_rpc_lock *)rpc;

        data = lyd_new(NULL, mod, "unlock");
        node = lyd_new(data, mod, "target");
        node = lyd_new_leaf(node, mod, ncds2str[rpc_lock->target], NULL);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        break;

    case NC_RPC_GET:
        rpc_g = (struct nc_rpc_get *)rpc;

        data = lyd_new(NULL, mod, "get");
        if (rpc_g->filter) {
            if (!rpc_g->filter[0] || (rpc_g->filter[0] == '<')) {
                node = lyd_new_anydata(data, mod, "filter", rpc_g->filter, LYD_ANYDATA_SXML);
                lyd_insert_attr(node, NULL, "type", "subtree");
            } else {
                node = lyd_new_anydata(data, mod, "filter", NULL, LYD_ANYDATA_CONSTSTRING);
                lyd_insert_attr(node, NULL, "type", "xpath");
                lyd_insert_attr(node, NULL, "select", rpc_g->filter);
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_g->wd_mode) {
            ietfncwd = ly_ctx_get_module(session->ctx, "ietf-netconf-with-defaults", NULL, 1);
            if (!ietfncwd) {
                ERR("Session %u: missing \"ietf-netconf-with-defaults\" schema in the context.", session->id);
                lyd_free(data);
                return NC_MSG_ERROR;
            }
            switch (rpc_g->wd_mode) {
            case NC_WD_ALL:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "report-all");
                break;
            case NC_WD_ALL_TAG:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "report-all-tagged");
                break;
            case NC_WD_TRIM:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "trim");
                break;
            case NC_WD_EXPLICIT:
                node = lyd_new_leaf(data, ietfncwd, "with-defaults", "explicit");
                break;
            default:
                /* cannot get here */
                node = NULL;
                break;
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_KILL:
        rpc_k = (struct nc_rpc_kill *)rpc;

        data = lyd_new(NULL, mod, "kill-session");
        sprintf(str, "%u", rpc_k->sid);
        lyd_new_leaf(data, mod, "session-id", str);
        break;

    case NC_RPC_COMMIT:
        rpc_com = (struct nc_rpc_commit *)rpc;

        data = lyd_new(NULL, mod, "commit");
        if (rpc_com->confirmed) {
            lyd_new_leaf(data, mod, "confirmed", NULL);
        }

        if (rpc_com->confirm_timeout) {
            sprintf(str, "%u", rpc_com->confirm_timeout);
            lyd_new_leaf(data, mod, "confirm-timeout", str);
        }

        if (rpc_com->persist) {
            node = lyd_new_leaf(data, mod, "persist", rpc_com->persist);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_com->persist_id) {
            node = lyd_new_leaf(data, mod, "persist-id", rpc_com->persist_id);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_DISCARD:
        data = lyd_new(NULL, mod, "discard-changes");
        break;

    case NC_RPC_CANCEL:
        rpc_can = (struct nc_rpc_cancel *)rpc;

        data = lyd_new(NULL, mod, "cancel-commit");
        if (rpc_can->persist_id) {
            node = lyd_new_leaf(data, mod, "persist-id", rpc_can->persist_id);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_VALIDATE:
        rpc_val = (struct nc_rpc_validate *)rpc;

        data = lyd_new(NULL, mod, "validate");
        node = lyd_new(data, mod, "source");
        if (rpc_val->url_config_src) {
            if (!rpc_val->url_config_src[0] || (rpc_val->url_config_src[0] == '<')) {
                node = lyd_new_anydata(node, mod, "config", rpc_val->url_config_src, LYD_ANYDATA_SXML);
            } else {
                node = lyd_new_leaf(node, mod, "url", rpc_val->url_config_src);
            }
        } else {
            node = lyd_new_leaf(node, mod, ncds2str[rpc_val->source], NULL);
        }
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        break;

    case NC_RPC_GETSCHEMA:
        rpc_gs = (struct nc_rpc_getschema *)rpc;

        data = lyd_new(NULL, mod, "get-schema");
        node = lyd_new_leaf(data, mod, "identifier", rpc_gs->identifier);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        if (rpc_gs->version) {
            node = lyd_new_leaf(data, mod, "version", rpc_gs->version);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        if (rpc_gs->format) {
            node = lyd_new_leaf(data, mod, "format", rpc_gs->format);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_SUBSCRIBE:
        rpc_sub = (struct nc_rpc_subscribe *)rpc;

        data = lyd_new(NULL, mod, "create-subscription");
        if (rpc_sub->stream) {
            node = lyd_new_leaf(data, mod, "stream", rpc_sub->stream);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_sub->filter) {
            if (!rpc_sub->filter[0] || (rpc_sub->filter[0] == '<')) {
                node = lyd_new_anydata(data, mod, "filter", rpc_sub->filter, LYD_ANYDATA_SXML);
                lyd_insert_attr(node, NULL, "type", "subtree");
            } else {
                node = lyd_new_anydata(data, mod, "filter", NULL, LYD_ANYDATA_CONSTSTRING);
                lyd_insert_attr(node, NULL, "type", "xpath");
                lyd_insert_attr(node, NULL, "select", rpc_sub->filter);
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_sub->start) {
            node = lyd_new_leaf(data, mod, "startTime", rpc_sub->start);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_sub->stop) {
            node = lyd_new_leaf(data, mod, "stopTime", rpc_sub->stop);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_GETDATA:
        rpc_getd = (struct nc_rpc_getdata *)rpc;

        data = lyd_new(NULL, mod, "get-data");
        node = lyd_new_leaf(data, mod, "datastore", rpc_getd->datastore);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        if (rpc_getd->filter) {
            if (!rpc_getd->filter[0] || (rpc_getd->filter[0] == '<')) {
                node = lyd_new_anydata(data, mod, "subtree-filter", rpc_getd->filter, LYD_ANYDATA_SXML);
            } else {
                node = lyd_new_leaf(data, mod, "xpath-filter", rpc_getd->filter);
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        if (rpc_getd->config_filter) {
            node = lyd_new_leaf(data, mod, "config-filter", rpc_getd->config_filter);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        for (i = 0; i < rpc_getd->origin_filter_count; ++i) {
            node = lyd_new_leaf(data, mod, rpc_getd->negated_origin_filter ? "negated-origin-filter" : "origin-filter",
                                rpc_getd->origin_filter[i]);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        if (rpc_getd->max_depth) {
            sprintf(str, "%u", rpc_getd->max_depth);
            node = lyd_new_leaf(data, mod, "max-depth", str);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        if (rpc_getd->with_origin) {
            node = lyd_new_leaf(data, mod, "with-origin", NULL);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (rpc_getd->wd_mode) {
            switch (rpc_getd->wd_mode) {
            case NC_WD_UNKNOWN:
                /* cannot get here */
                break;
            case NC_WD_ALL:
                /* "with-defaults" are used from a grouping so it belongs to the ietf-netconf-nmda module */
                node = lyd_new_leaf(data, mod, "with-defaults", "report-all");
                break;
            case NC_WD_ALL_TAG:
                node = lyd_new_leaf(data, mod, "with-defaults", "report-all-tagged");
                break;
            case NC_WD_TRIM:
                node = lyd_new_leaf(data, mod, "with-defaults", "trim");
                break;
            case NC_WD_EXPLICIT:
                node = lyd_new_leaf(data, mod, "with-defaults", "explicit");
                break;
            }
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }
        break;

    case NC_RPC_EDITDATA:
        rpc_editd = (struct nc_rpc_editdata *)rpc;

        data = lyd_new(NULL, mod, "edit-data");
        node = lyd_new_leaf(data, mod, "datastore", rpc_editd->datastore);
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }

        if (rpc_editd->default_op) {
            node = lyd_new_leaf(data, mod, "default-operation", rpcedit_dfltop2str[rpc_editd->default_op]);
            if (!node) {
                lyd_free(data);
                return NC_MSG_ERROR;
            }
        }

        if (!rpc_editd->edit_cont[0] || (rpc_editd->edit_cont[0] == '<')) {
            node = lyd_new_anydata(data, mod, "config", rpc_editd->edit_cont, LYD_ANYDATA_SXML);
        } else {
            node = lyd_new_leaf(data, mod, "url", rpc_editd->edit_cont);
        }
        if (!node) {
            lyd_free(data);
            return NC_MSG_ERROR;
        }
        break;

    default:
        ERRINT;
        return NC_MSG_ERROR;
    }

    if (!data) {
        /* error was already printed */
        return NC_MSG_ERROR;
    }

    if (lyd_validate(&data, LYD_OPT_RPC | LYD_OPT_NOEXTDEPS
                     | (session->flags & NC_SESSION_CLIENT_NOT_STRICT ? 0 : LYD_OPT_STRICT), NULL)) {
        if (dofree) {
            lyd_free(data);
        }
        return NC_MSG_ERROR;
    }

    /* send RPC, store its message ID */
    r = nc_send_msg_io(session, timeout, data);
    cur_msgid = session->opts.client.msgid;

    if (dofree) {
        lyd_free(data);
    }

    if (r == NC_MSG_RPC) {
        *msgid = cur_msgid;
    }
    return r;
}

API void
nc_client_session_set_not_strict(struct nc_session *session)
{
    if (session->side != NC_CLIENT) {
        ERRARG("session");
        return;
    }

    session->flags |= NC_SESSION_CLIENT_NOT_STRICT;
}
