client CHANGE rewrite loading the schemas into context when connecting to a server

- allow setting schema callback to get schema via callers function
- change priorities: 1) searchpath, 2) callback, 3) get-schema
- schemas retrieved via get-schema are stored into the searchpath

Fixes #26
Fixes #27
diff --git a/src/libnetconf.h b/src/libnetconf.h
index e1066ea..50104b2 100644
--- a/src/libnetconf.h
+++ b/src/libnetconf.h
@@ -98,17 +98,18 @@
  * Client
  * ------
  *
- * Optionally, a client can use nc_client_set_schema_searchpath()
- * to set the path to a directory with modules that will be loaded from there if they
- * could not be downloaded from the server (it does not support \<get-schema\>).
- * However, to be able to create at least the \<get-schema\> RPC, this directory must
- * contain the module _ietf-netconf-monitoring_. If this directory is not set,
- * the default _libnetconf2_ schema directory is used that includes this module
- * and a few others.
+ * Optionally, a client can specify two alternative ways to get schemas needed when connecting
+ * with a server. The primary way is to read local files in searchpath (and its subdirectories)
+ * specified via nc_client_set_schema_searchpath(). Alternatively, _libnetconf2_ can use callback
+ * provided via nc_client_set_schema_callback(). If these ways do not succeed and the server
+ * implements NETCONF \<get-schema\> operation, the schema is retrieved from the server and stored
+ * localy into the searchpath (if specified) for a future use. If none of these methods succeed to
+ * load particular schema, the data from this schema are ignored during the communication with the
+ * server.
  *
- * There are many other @ref howtoclientssh "SSH", @ref howtoclienttls "TLS" and @ref howtoclientch
- * "Call Home" getter/setter functions to manipulate with various settings. All these settings (including
- * the searchpath) are internally placed in a thread-specific context so they are independent and
+ * Besides the mentioned setters, there are many other @ref howtoclientssh "SSH", @ref howtoclienttls "TLS"
+ * and @ref howtoclientch "Call Home" getter/setter functions to manipulate with various settings. All these
+ * settings are internally placed in a thread-specific context so they are independent and
  * initialized to the default values within each new thread. However, the context can be shared among
  * the threads using nc_client_get_thread_context() and nc_client_set_thread_context() functions. In such
  * a case, be careful and avoid concurrent execution of the mentioned setters/getters and functions
@@ -137,6 +138,8 @@
  *
  * - nc_client_get_schema_searchpath()
  * - nc_client_set_schema_searchpath()
+ * - nc_client_get_schema_callback()
+ * - nc_client_set_schema_callback()
  *
  * - nc_client_get_thread_context()
  * - nc_client_set_thread_context()
diff --git a/src/session_client.c b/src/session_client.c
index 7e88d8e..96a526f 100644
--- a/src/session_client.c
+++ b/src/session_client.c
@@ -12,6 +12,7 @@
  *     https://opensource.org/licenses/BSD-3-Clause
  */
 
+#define _GNU_SOURCE
 #include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
@@ -187,6 +188,33 @@
     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);
+        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);
+        }
+        ly_ctx_set_searchdir(ctx, SCHEMAS_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)
 {
@@ -213,90 +241,28 @@
     return client_opts.schema_searchpath;
 }
 
-/* SCHEMAS_DIR not used (implicitly) */
-static int
-ctx_check_and_load_model(struct nc_session *session, const char *module_cpblt)
+API int
+nc_client_set_schema_callback(ly_module_imp_clb clb, void *user_data)
 {
-    const struct lys_module *module;
-    char *ptr, *ptr2;
-    char *model_name, *revision = NULL, *features = NULL;
-
-    assert(!strncmp(module_cpblt, "module=", 7));
-
-    ptr = (char *)module_cpblt + 7;
-    ptr2 = strchr(ptr, '&');
-    if (!ptr2) {
-        ptr2 = ptr + strlen(ptr);
-    }
-    model_name = strndup(ptr, ptr2 - ptr);
-
-    /* parse revision */
-    ptr = strstr(module_cpblt, "revision=");
-    if (ptr) {
-        ptr += 9;
-        ptr2 = strchr(ptr, '&');
-        if (!ptr2) {
-            ptr2 = ptr + strlen(ptr);
-        }
-        revision = strndup(ptr, ptr2 - ptr);
-    }
-
-    /* load module if needed */
-    module = ly_ctx_get_module(session->ctx, model_name, revision);
-    if (!module) {
-        module = ly_ctx_load_module(session->ctx, model_name, revision);
-    }
-
-    free(revision);
-    if (!module) {
-        WRN("Failed to load model \"%s\".", model_name);
-        free(model_name);
-        return 1;
-    }
-    free(model_name);
-
-    /* make it implemented */
-    if (!module->implemented && lys_set_implemented(module)) {
-        WRN("Failed to implement model \"%s\".", module->name);
-        return 1;
-    }
-
-    /* parse features */
-    ptr = strstr(module_cpblt, "features=");
-    if (ptr) {
-        ptr += 9;
-        ptr2 = strchr(ptr, '&');
-        if (!ptr2) {
-            ptr2 = ptr + strlen(ptr);
-        }
-        features = strndup(ptr, ptr2 - ptr);
-    }
-
-    /* enable features */
-    if (features) {
-        /* basically manual strtok_r (to avoid macro) */
-        ptr2 = features;
-        for (ptr = features; *ptr; ++ptr) {
-            if (*ptr == ',') {
-                *ptr = '\0';
-                /* remember last feature */
-                ptr2 = ptr + 1;
-            }
-        }
-
-        ptr = features;
-        lys_features_enable(module, ptr);
-        while (ptr != ptr2) {
-            ptr += strlen(ptr) + 1;
-            lys_features_enable(module, ptr);
-        }
-
-        free(features);
+    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;
+}
+
 /* SCHEMAS_DIR used as the last resort */
 static int
 ctx_check_and_load_ietf_netconf(struct ly_ctx *ctx, char **cpblts)
@@ -343,7 +309,7 @@
 }
 
 static char *
-libyang_module_clb(const char *mod_name, const char *mod_rev, const char *submod_name, const char *submod_rev,
+getschema_module_clb(const char *mod_name, const char *mod_rev, const char *submod_name, const char *submod_rev,
                    void *user_data, LYS_INFORMAT *format, void (**free_model_data)(void *model_data))
 {
     struct nc_session *session = (struct nc_session *)user_data;
@@ -355,6 +321,9 @@
     NC_MSG_TYPE msg;
     char *model_data = NULL;
     uint64_t msgid;
+    char *filename = NULL;
+    const char * const *searchdirs;
+    FILE *output;
 
     if (submod_name) {
         rpc = nc_rpc_getschema(submod_name, submod_rev, "yang", NC_PARAMTYPE_CONST);
@@ -437,84 +406,414 @@
     *free_model_data = free;
     *format = LYS_IN_YANG;
 
+    /* try to store the model_data into local schema repository */
+    if (model_data) {
+        searchdirs = ly_ctx_get_searchdirs(session->ctx);
+        asprintf(&filename, "%s/%s%s%s.yang", searchdirs ? searchdirs[0] : ".", mod_name,
+                 mod_rev ? "@" : "", mod_rev ? mod_rev : "");
+        output = fopen(filename, "w");
+        if (!output) {
+            WRN("Unable to store \"%s\" as a local copy of schema retreived via <get-schema> (%s).",
+                filename, strerror(errno));
+        } else {
+            fputs(model_data, output);
+            fclose(output);
+        }
+        free(filename);
+    }
+
     return model_data;
 }
 
+/* SCHEMAS_DIR not used (implicitly) */
+static int
+nc_ctx_fill_cpblts(struct nc_session *session, ly_module_imp_clb user_clb, void *user_data)
+{
+    int ret = 1;
+    LY_LOG_LEVEL verb;
+    const struct lys_module *mod;
+    char *ptr, *ptr2;
+    const char *module_cpblt;
+    char *name = NULL, *revision = NULL, *features = NULL;
+    unsigned int u;
+
+    for (u = 0; session->opts.client.cpblts[u]; ++u) {
+        module_cpblt = strstr(session->opts.client.cpblts[u], "module=");
+        /* this capability requires a module */
+        if (!module_cpblt) {
+            continue;
+        }
+
+        /* get module name */
+        ptr = (char *)module_cpblt + 7;
+        ptr2 = strchr(ptr, '&');
+        if (!ptr2) {
+            ptr2 = ptr + strlen(ptr);
+        }
+        free(name);
+        name = strndup(ptr, ptr2 - ptr);
+
+        /* get module revision */
+        free(revision); revision = NULL;
+        ptr = strstr(module_cpblt, "revision=");
+        if (ptr) {
+            ptr += 9;
+            ptr2 = strchr(ptr, '&');
+            if (!ptr2) {
+                ptr2 = ptr + strlen(ptr);
+            }
+            revision = strndup(ptr, ptr2 - ptr);
+        }
+
+        mod = ly_ctx_get_module(session->ctx, name, revision);
+        if (mod) {
+            if (!mod->implemented) {
+                /* make the present module implemented */
+                if (lys_set_implemented(mod)) {
+                    ERR("Failed to implement model \"%s\".", mod->name);
+                    goto cleanup;
+                }
+            }
+        } else {
+            /* missing implemented module, load it ... */
+
+            /* expecting errors here */
+            verb = ly_verb(LY_LLSILENT);
+
+            /* 1) using only searchpaths */
+            mod = ly_ctx_load_module(session->ctx, name, revision);
+
+            /* 2) using user callback */
+            if (!mod && user_clb) {
+                ly_ctx_set_module_imp_clb(session->ctx, user_clb, user_data);
+                mod = ly_ctx_load_module(session->ctx, name, revision);
+            }
+
+            /* 3) using get-schema callback */
+            if (!mod) {
+                ly_ctx_set_module_imp_clb(session->ctx, &getschema_module_clb, session);
+                mod = ly_ctx_load_module(session->ctx, name, revision);
+            }
+
+            /* revert the changed verbosity level */
+            ly_verb(verb);
+
+            /* unset callback back to use searchpath */
+            ly_ctx_set_module_imp_clb(session->ctx, NULL, NULL);
+        }
+
+        if (!mod) {
+            /* all loading ways failed, the schema will be ignored in the received data */
+            WRN("Failed to load schema \"%s@%s\".", name, revision ? revision : "<latest>");
+            session->flags |= NC_SESSION_CLIENT_NOT_STRICT;
+
+            /* TODO: maybe re-print hidden error messages */
+        } else {
+            /* set features - first disable all to enable specified then */
+            lys_features_disable(mod, "*");
+
+            ptr = strstr(module_cpblt, "features=");
+            if (ptr) {
+                ptr += 9;
+                ptr2 = strchr(ptr, '&');
+                if (!ptr2) {
+                    ptr2 = ptr + strlen(ptr);
+                }
+                free(features);
+                features = strndup(ptr, ptr2 - ptr);
+
+                /* basically manual strtok_r (to avoid macro) */
+                ptr2 = features;
+                for (ptr = features; *ptr; ++ptr) {
+                    if (*ptr == ',') {
+                        *ptr = '\0';
+                        /* remember last feature */
+                        ptr2 = ptr + 1;
+                    }
+                }
+
+                ptr = features;
+                while (1) {
+                    lys_features_enable(mod, ptr);
+                    if (ptr != ptr2) {
+                        ptr += strlen(ptr) + 1;
+                    } else {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    ret = 0;
+
+cleanup:
+    free(name);
+    free(revision);
+    free(features);
+
+    return ret;
+}
+
+static int
+nc_ctx_fill_yl(struct nc_session *session, ly_module_imp_clb user_clb, void *user_data)
+{
+    int ret = 1;
+    LY_LOG_LEVEL verb;
+    struct nc_rpc *rpc = NULL;
+    struct nc_reply *reply = NULL;
+    struct nc_reply_error *error_rpl;
+    struct nc_reply_data *data_rpl;
+    NC_MSG_TYPE msg;
+    uint64_t msgid;
+    struct lyd_node *data = NULL, *iter;
+    struct ly_set *modules = NULL, *imports = NULL, *features = NULL;
+    unsigned int u, v;
+    const char *name, *revision;
+    int implemented, imports_flag = 0;
+    const struct lys_module *mod;
+
+    /* get yang-library data from the server */
+    rpc = nc_rpc_get("/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) {
+        ERR("Session %u: failed to send request for yang-library data, trying to use capabilities list.",
+            session->id);
+        goto cleanup;
+    }
+
+    do {
+        msg = nc_recv_reply(session, rpc, msgid, NC_READ_ACT_TIMEOUT * 1000, 0, &reply);
+    } while (msg == NC_MSG_NOTIF);
+    if (msg == NC_MSG_WOULDBLOCK) {
+        ERR("Session %u: timeout for receiving reply to a <get> yang-library data expired.", session->id);
+        goto cleanup;
+    } else if (msg == NC_MSG_ERROR) {
+        ERR("Session %u: failed to receive a reply to <get> of yang-library data.", session->id);
+        goto cleanup;
+    }
+
+    switch (reply->type) {
+    case NC_RPL_OK:
+        ERR("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) {
+            ERR("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 {
+            ERR("Session %u: unexpected reply error to a yang-library <get> RPC.", session->id);
+        }
+        goto cleanup;
+    case NC_RPL_NOTIF:
+        ERR("Session %u: unexpected reply notification to a yang-library <get> RPC.", session->id);
+        goto cleanup;
+    }
+
+    data_rpl = (struct nc_reply_data *)reply;
+    if (!data_rpl->data || strcmp(data_rpl->data->schema->module->name, "ietf-yang-library") ||
+        strcmp(data_rpl->data->schema->name, "modules-state")) {
+        ERR("Session %u: unexpected data in reply to a yang-library <get> RPC.", session->id);
+        goto cleanup;
+    }
+
+    modules = lyd_find_xpath(data_rpl->data, "/ietf-yang-library:modules-state/module");
+    if (!modules || !modules->number) {
+        ERR("No yang-library modules information for session %u.", session->id);
+        goto cleanup;
+    }
+
+    features = ly_set_new();
+    imports = ly_set_new();
+
+parse:
+    for (u = modules->number - 1; u < modules->number; u--) {
+        name = revision = NULL;
+        ly_set_clean(features);
+        implemented = 0;
+
+        /* store the data */
+        LY_TREE_FOR(modules->set.d[u]->child, iter) {
+            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")) {
+                name = ((struct lyd_node_leaf_list *)iter)->value_str;
+            } else if (!strcmp(iter->schema->name, "revision")) {
+                revision = ((struct lyd_node_leaf_list *)iter)->value_str;
+            } else if (!strcmp(iter->schema->name, "conformance-type")) {
+                implemented = !strcmp(((struct lyd_node_leaf_list *)iter)->value_str, "implement");
+            } else if (!strcmp(iter->schema->name, "feature")) {
+                ly_set_add(features, (void*)((struct lyd_node_leaf_list *)iter)->value_str, LY_SET_OPT_USEASLIST);
+            }
+        }
+
+        mod = ly_ctx_get_module(session->ctx, name, revision);
+        if (mod) {
+            if (implemented && !mod->implemented) {
+                /* make the present module implemented */
+                if (lys_set_implemented(mod)) {
+                    ERR("Failed to implement model \"%s\".", mod->name);
+                    ret = -1; /* fatal error, not caused just by missing schema but by something in the context */
+                    goto cleanup;
+                }
+            }
+        } else if (!mod && implemented) {
+            /* missing implemented module, load it ... */
+
+            /* expecting errors here */
+            verb = ly_verb(LY_LLSILENT);
+
+            /* 1) using only searchpaths */
+            mod = ly_ctx_load_module(session->ctx, name, revision);
+
+            /* 2) using user callback */
+            if (!mod && user_clb) {
+                ly_ctx_set_module_imp_clb(session->ctx, user_clb, user_data);
+                mod = ly_ctx_load_module(session->ctx, name, revision);
+            }
+
+            /* 3) using get-schema callback */
+            if (!mod) {
+                ly_ctx_set_module_imp_clb(session->ctx, &getschema_module_clb, session);
+                mod = ly_ctx_load_module(session->ctx, name, revision);
+            }
+
+            /* revert the changed verbosity level */
+            ly_verb(verb);
+
+            /* unset callback back to use searchpath */
+            ly_ctx_set_module_imp_clb(session->ctx, NULL, NULL);
+        } else { /* !mod && !implemented - will be loaded automatically, but remember to set features in the end */
+            assert(!imports_flag);
+            ly_set_add(imports, modules->set.d[u], LY_SET_OPT_USEASLIST);
+            continue;
+        }
+
+        if (!mod) {
+            /* all loading ways failed, the schema will be ignored in the received data */
+            WRN("Failed to load schema \"%s@%s\".", name, revision ? revision : "<latest>");
+            session->flags |= NC_SESSION_CLIENT_NOT_STRICT;
+
+            /* TODO: maybe re-print hidden error messages */
+        } else {
+            /* set features - first disable all to enable specified then */
+            lys_features_disable(mod, "*");
+            for (v = 0; v < features->number; v++) {
+                lys_features_enable(mod, (const char*)features->set.g[v]);
+            }
+        }
+    }
+
+    if (!imports_flag && imports->number) {
+        /* even imported modules should be now loaded as dependency, so just go through
+         * the parsing again and just set the features */
+        ly_set_free(modules);
+        modules = imports;
+        imports = NULL;
+        imports_flag = 1;
+        goto parse;
+    }
+
+    /* done */
+    ret = 0;
+
+cleanup:
+    nc_rpc_free(rpc);
+    nc_reply_free(reply);
+    lyd_free_withsiblings(data);
+
+    ly_set_free(modules);
+    ly_set_free(imports);
+    ly_set_free(features);
+
+    return ret;
+}
+
 int
 nc_ctx_check_and_fill(struct nc_session *session)
 {
-    const char *module_cpblt;
-    int i, get_schema_support = 0, ret = 0, r;
+    int i, get_schema_support = 0, yanglib_support = 0, ret = -1, r;
     ly_module_imp_clb old_clb = NULL;
     void *old_data = NULL;
 
     assert(session->opts.client.cpblts && session->ctx);
 
+    /* store the original user's callback, here we will be switching between searchpath, user callback
+     * and get-schema callback */
+    old_clb = ly_ctx_get_module_imp_clb(session->ctx, &old_data);
+    ly_ctx_set_module_imp_clb(session->ctx, NULL, NULL); /* unset callback, so we prefer local searchpath */
+
     /* check if get-schema 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", 51)) {
+        if (!strncmp(session->opts.client.cpblts[i], "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring?", 52)) {
             get_schema_support = 1;
-            break;
+            if (yanglib_support) {
+                break;
+            }
+        } else if (!strncmp(session->opts.client.cpblts[i], "urn:ietf:params:xml:ns:yang:ietf-yang-library?", 46)) {
+            yanglib_support = 1;
+            if (get_schema_support) {
+                break;
+            }
         }
     }
 
     /* get-schema is supported, load local ietf-netconf-monitoring so we can create <get-schema> RPCs */
     if (get_schema_support && !ly_ctx_get_module(session->ctx, "ietf-netconf-monitoring", NULL)) {
-        if (lys_parse_path(session->ctx, SCHEMAS_DIR"/ietf-netconf-monitoring.yin", LYS_IN_YIN)) {
-            /* set module retrieval using <get-schema> */
-            old_clb = ly_ctx_get_module_imp_clb(session->ctx, &old_data);
-            ly_ctx_set_module_imp_clb(session->ctx, libyang_module_clb, session);
-        } else {
+        if (!lys_parse_path(session->ctx, SCHEMAS_DIR"/ietf-netconf-monitoring.yin", LYS_IN_YIN)) {
             WRN("Loading NETCONF monitoring schema failed, cannot use <get-schema>.");
+            get_schema_support = 0;
         }
     }
+    /* yang-library present does not need to be checked, it is one of the libyang's internal modules,
+     * so it is always present */
 
     /* load base model disregarding whether it's in capabilities (but NETCONF capabilities are used to enable features) */
     if (ctx_check_and_load_ietf_netconf(session->ctx, session->opts.client.cpblts)) {
-        if (old_clb) {
-            ly_ctx_set_module_imp_clb(session->ctx, old_clb, old_data);
-        }
-        return -1;
+        goto cleanup;
     }
 
-    /* load all other models */
-    for (i = 0; session->opts.client.cpblts[i]; ++i) {
-        module_cpblt = strstr(session->opts.client.cpblts[i], "module=");
-        /* this capability requires a module */
-        if (module_cpblt) {
-            r = ctx_check_and_load_model(session, module_cpblt);
-            if (r == -1) {
-                ret = -1;
-                break;
-            }
+    if (yanglib_support && get_schema_support) {
+        /* load schemas according to the ietf-yang-library data, which are more precise than capabilities list */
+        r = nc_ctx_fill_yl(session, old_clb, old_data);
+        if (r == -1) {
+            goto cleanup;
+        } else if (r == 1) {
+            /* try to use standard capabilities */
+            goto capabilities;
+        }
+    } else {
+capabilities:
 
-            /* failed to load schema, but let's try to find it using user callback (or locally, if not set),
-            * if it was using get-schema */
-            if (r == 1) {
-                if (get_schema_support) {
-                    VRB("Trying to load the schema from a different source.");
-                    /* works even if old_clb is NULL */
-                    ly_ctx_set_module_imp_clb(session->ctx, old_clb, old_data);
-                    r = ctx_check_and_load_model(session, module_cpblt);
-                }
-
-                /* fail again (or no other way to try), too bad */
-                if (r) {
-                    session->flags |= NC_SESSION_CLIENT_NOT_STRICT;
-                }
-
-                /* set get-schema callback back */
-                ly_ctx_set_module_imp_clb(session->ctx, &libyang_module_clb, session);
-            }
+        r = nc_ctx_fill_cpblts(session, old_clb, old_data);
+        if (r) {
+            goto cleanup;
         }
     }
 
-    if (old_clb) {
-        ly_ctx_set_module_imp_clb(session->ctx, old_clb, old_data);
-    }
+    /* succsess */
+    ret = 0;
+
     if (session->flags & NC_SESSION_CLIENT_NOT_STRICT) {
         WRN("Some models failed to be loaded, any data from these models (and any other unknown) will be ignored.");
     }
+
+cleanup:
+    /* set user callback back */
+    ly_ctx_set_module_imp_clb(session->ctx, old_clb, old_data);
+
     return ret;
 }
 
diff --git a/src/session_client.h b/src/session_client.h
index 0ee2015..559dc6c 100644
--- a/src/session_client.h
+++ b/src/session_client.h
@@ -44,6 +44,12 @@
  * The location is searched when connecting to a NETCONF server and building
  * YANG context for further processing of the NETCONF messages and data.
  *
+ * The searchpath is also used to store schemas retreived via \<get-schema\>
+ * operation - if the schema is not found in searchpath neither via schema
+ * callback provided via nc_client_set_schema_callback() and server supports
+ * the NETCONF \<get-schema\> operation, the schema is retrieved this way and
+ * stored into the searchpath (if specified).
+ *
  * @param[in] path Directory where to search for YANG/YIN schemas.
  * @return 0 on success, 1 on (memory allocation) failure.
  */
@@ -57,13 +63,33 @@
 const char *nc_client_get_schema_searchpath(void);
 
 /**
+ * @brief Set callback function to get missing schemas.
+ *
+ * @param[in] clb Callback responsible for returning the missing model.
+ * @param[in] user_data Arbitrary data that will always be passed to the callback \p clb.
+ * @return 0 on success, 1 on (memory allocation) failure.
+ */
+int nc_client_set_schema_callback(ly_module_imp_clb clb, void *user_data);
+
+/**
+ * @brief Get callback function used to get missing schemas.
+ *
+ * @param[out] user_data Optionally return the private data set with the callback.
+ * Note that the caller is responsible for freeing the private data, so before
+ * changing the callback, private data used for the previous callback should be
+ * freed.
+ * @return Pointer to the set callback, NULL if no such callback was set.
+ */
+ly_module_imp_clb nc_client_get_schema_callback(void **user_data);
+
+/**
  * @brief Use the provided thread-specific client's context in the current thread.
  *
  * Note that from this point the context is shared with the thread from which the context was taken and any
  * nc_client_*set* functions and functions creating connection in these threads should be protected from the
  * concurrent execution.
  *
- * Context contains schema searchpath, call home binds, TLS and SSH authentication data (username, keys,
+ * Context contains schema searchpath/callback, call home binds, TLS and SSH authentication data (username, keys,
  * various certificates and callbacks).
  *
  * @param[in] context Client's thread-specific context provided by nc_client_get_thread_context().
diff --git a/src/session_client_ssh.c b/src/session_client_ssh.c
index e77aec7..1630427 100644
--- a/src/session_client_ssh.c
+++ b/src/session_client_ssh.c
@@ -45,6 +45,7 @@
 #include "libnetconf.h"
 
 struct nc_client_context *nc_client_context_location(void);
+int nc_session_new_ctx(struct nc_session *session, struct ly_ctx *ctx);
 
 #define client_opts nc_client_context_location()->opts
 #define ssh_opts nc_client_context_location()->ssh_opts
@@ -1485,22 +1486,10 @@
      * SSH session is established and netconf channel opened, create a NETCONF session. (Application layer)
      */
 
-    /* assign context (dicionary needed for handshake) */
-    if (!ctx) {
-        if (client_opts.schema_searchpath) {
-            ctx = ly_ctx_new(client_opts.schema_searchpath);
-        } else {
-            ctx = ly_ctx_new(SCHEMAS_DIR);
-        }
-        /* definitely should not happen, but be ready */
-        if (!ctx && !(ctx = ly_ctx_new(NULL))) {
-            /* that's just it */
-            goto fail;
-        }
-    } else {
-        session->flags |= NC_SESSION_SHAREDCTX;
+    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
+        goto fail;
     }
-    session->ctx = ctx;
+    ctx = session->ctx;
 
     /* NETCONF handshake */
     if (nc_handshake(session) != NC_MSG_HELLO) {
@@ -1613,22 +1602,10 @@
         goto fail;
     }
 
-    /* assign context (dicionary needed for handshake) */
-    if (!ctx) {
-        if (client_opts.schema_searchpath) {
-            ctx = ly_ctx_new(client_opts.schema_searchpath);
-        } else {
-            ctx = ly_ctx_new(SCHEMAS_DIR);
-        }
-        /* definitely should not happen, but be ready */
-        if (!ctx && !(ctx = ly_ctx_new(NULL))) {
-            /* that's just it */
-            goto fail;
-        }
-    } else {
-        session->flags |= NC_SESSION_SHAREDCTX;
+    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
+        goto fail;
     }
-    session->ctx = ctx;
+    ctx = session->ctx;
 
     /* NETCONF handshake */
     if (nc_handshake(session) != NC_MSG_HELLO) {
@@ -1694,17 +1671,10 @@
         goto fail;
     }
 
-    /* assign context (dicionary needed for handshake) */
-    if (!ctx) {
-        if (client_opts.schema_searchpath) {
-            ctx = ly_ctx_new(client_opts.schema_searchpath);
-        } else {
-            ctx = ly_ctx_new(SCHEMAS_DIR);
-        }
-    } else {
-        new_session->flags |= NC_SESSION_SHAREDCTX;
+    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
+        goto fail;
     }
-    new_session->ctx = ctx;
+    ctx = session->ctx;
 
     /* NETCONF handshake */
     if (nc_handshake(new_session) != NC_MSG_HELLO) {
diff --git a/src/session_client_tls.c b/src/session_client_tls.c
index 08b70c3..a4bf8ba 100644
--- a/src/session_client_tls.c
+++ b/src/session_client_tls.c
@@ -30,6 +30,7 @@
 #include "libnetconf.h"
 
 struct nc_client_context *nc_client_context_location(void);
+int nc_session_new_ctx( struct nc_session *session, struct ly_ctx *ctx);
 
 #define client_opts nc_client_context_location()->opts
 #define tls_opts nc_client_context_location()->tls_opts
@@ -669,22 +670,10 @@
         WRN("Server certificate verification problem (%s).", X509_verify_cert_error_string(verify));
     }
 
-    /* assign context (dicionary needed for handshake) */
-    if (!ctx) {
-        if (client_opts.schema_searchpath) {
-            ctx = ly_ctx_new(client_opts.schema_searchpath);
-        } else {
-            ctx = ly_ctx_new(SCHEMAS_DIR);
-        }
-        /* definitely should not happen, but be ready */
-        if (!ctx && !(ctx = ly_ctx_new(NULL))) {
-            /* that's just it */
-            goto fail;
-        }
-    } else {
-        session->flags |= NC_SESSION_SHAREDCTX;
+    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
+        goto fail;
     }
-    session->ctx = ctx;
+    ctx = session->ctx;
 
     /* NETCONF handshake */
     if (nc_handshake(session) != NC_MSG_HELLO) {
@@ -738,22 +727,10 @@
     session->ti_type = NC_TI_OPENSSL;
     session->ti.tls = tls;
 
-    /* assign context (dicionary needed for handshake) */
-    if (!ctx) {
-        if (client_opts.schema_searchpath) {
-            ctx = ly_ctx_new(client_opts.schema_searchpath);
-        } else {
-            ctx = ly_ctx_new(SCHEMAS_DIR);
-        }
-        /* definitely should not happen, but be ready */
-        if (!ctx && !(ctx = ly_ctx_new(NULL))) {
-            /* that's just it */
-            goto fail;
-        }
-    } else {
-        session->flags |= NC_SESSION_SHAREDCTX;
+    if (nc_session_new_ctx(session, ctx) != EXIT_SUCCESS) {
+        goto fail;
     }
-    session->ctx = ctx;
+    ctx = session->ctx;
 
     /* NETCONF handshake */
     if (nc_handshake(session) != NC_MSG_HELLO) {
diff --git a/src/session_p.h b/src/session_p.h
index d051e6d..916f2c2 100644
--- a/src/session_p.h
+++ b/src/session_p.h
@@ -125,6 +125,8 @@
 /* ACCESS unlocked */
 struct nc_client_opts {
     char *schema_searchpath;
+    ly_module_imp_clb schema_clb;
+    void *schema_clb_data;
 
     struct nc_bind {
         const char *address;