summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/responder/ifp/ifp_iface.xml9
-rw-r--r--src/responder/ifp/ifp_iface_generated.c20
-rw-r--r--src/responder/ifp/ifp_iface_generated.h2
-rw-r--r--src/responder/ifp/ifp_private.h2
-rw-r--r--src/responder/ifp/ifpsrv.c1
-rw-r--r--src/responder/ifp/ifpsrv_cmd.c563
6 files changed, 597 insertions, 0 deletions
diff --git a/src/responder/ifp/ifp_iface.xml b/src/responder/ifp/ifp_iface.xml
index 078d29e2d..de6acce2f 100644
--- a/src/responder/ifp/ifp_iface.xml
+++ b/src/responder/ifp/ifp_iface.xml
@@ -3,9 +3,18 @@
<node>
<interface name="org.freedesktop.sssd.infopipe">
<annotation value="infopipe_iface" name="org.freedesktop.DBus.GLib.CSymbol"/>
+
<method name="Ping">
<!-- manual argument parsing, raw handler -->
<annotation name="org.freedesktop.sssd.RawHandler" value="true"/>
</method>
+
+ <method name="GetUserAttr">
+ <arg name="user" type="s" direction="in" />
+ <arg name="attr" type="as" direction="in" />
+ <arg name="values" type="a{sv}" direction="out"/>
+ <annotation name="org.freedesktop.sssd.RawHandler" value="true"/>
+ </method>
+
</interface>
</node>
diff --git a/src/responder/ifp/ifp_iface_generated.c b/src/responder/ifp/ifp_iface_generated.c
index 57c67f8bc..ed8d65187 100644
--- a/src/responder/ifp/ifp_iface_generated.c
+++ b/src/responder/ifp/ifp_iface_generated.c
@@ -5,6 +5,19 @@
#include "sbus/sssd_dbus_meta.h"
#include "ifp_iface_generated.h"
+/* arguments for org.freedesktop.sssd.infopipe.GetUserAttr */
+const struct sbus_arg_meta infopipe_iface_GetUserAttr__in[] = {
+ { "user", "s" },
+ { "attr", "as" },
+ { NULL, }
+};
+
+/* arguments for org.freedesktop.sssd.infopipe.GetUserAttr */
+const struct sbus_arg_meta infopipe_iface_GetUserAttr__out[] = {
+ { "values", "a{sv}" },
+ { NULL, }
+};
+
/* methods for org.freedesktop.sssd.infopipe */
const struct sbus_method_meta infopipe_iface__methods[] = {
{
@@ -14,6 +27,13 @@ const struct sbus_method_meta infopipe_iface__methods[] = {
offsetof(struct infopipe_iface, Ping),
NULL, /* no invoker */
},
+ {
+ "GetUserAttr", /* name */
+ infopipe_iface_GetUserAttr__in,
+ infopipe_iface_GetUserAttr__out,
+ offsetof(struct infopipe_iface, GetUserAttr),
+ NULL, /* no invoker */
+ },
{ NULL, }
};
diff --git a/src/responder/ifp/ifp_iface_generated.h b/src/responder/ifp/ifp_iface_generated.h
index f69fb162d..c52e87f06 100644
--- a/src/responder/ifp/ifp_iface_generated.h
+++ b/src/responder/ifp/ifp_iface_generated.h
@@ -14,6 +14,7 @@
/* constants for org.freedesktop.sssd.infopipe */
#define INFOPIPE_IFACE "org.freedesktop.sssd.infopipe"
#define INFOPIPE_IFACE_PING "Ping"
+#define INFOPIPE_IFACE_GETUSERATTR "GetUserAttr"
/* ------------------------------------------------------------------------
* DBus handlers
@@ -37,6 +38,7 @@
struct infopipe_iface {
struct sbus_vtable vtable; /* derive from sbus_vtable */
sbus_msg_handler_fn Ping;
+ sbus_msg_handler_fn GetUserAttr;
};
/* ------------------------------------------------------------------------
diff --git a/src/responder/ifp/ifp_private.h b/src/responder/ifp/ifp_private.h
index e44b27bf4..52c480bb4 100644
--- a/src/responder/ifp/ifp_private.h
+++ b/src/responder/ifp/ifp_private.h
@@ -49,6 +49,8 @@ struct ifp_ctx {
* It will be removed later */
int ifp_ping(struct sbus_request *dbus_req, void *data);
+int ifp_user_get_attr(struct sbus_request *dbus_req, void *data);
+
/* == Utility functions == */
struct ifp_req {
struct sbus_request *dbus_req;
diff --git a/src/responder/ifp/ifpsrv.c b/src/responder/ifp/ifpsrv.c
index f9dc69057..978f3614a 100644
--- a/src/responder/ifp/ifpsrv.c
+++ b/src/responder/ifp/ifpsrv.c
@@ -66,6 +66,7 @@ static struct data_provider_iface ifp_dp_methods = {
struct infopipe_iface ifp_iface = {
{ &infopipe_iface_meta, 0 },
.Ping = ifp_ping,
+ .GetUserAttr = ifp_user_get_attr,
};
struct sss_cmd_table *get_ifp_cmds(void)
diff --git a/src/responder/ifp/ifpsrv_cmd.c b/src/responder/ifp/ifpsrv_cmd.c
index e26bcfa58..2fc4308b4 100644
--- a/src/responder/ifp/ifpsrv_cmd.c
+++ b/src/responder/ifp/ifpsrv_cmd.c
@@ -20,8 +20,571 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+#include "db/sysdb.h"
+
#include "responder/ifp/ifp_private.h"
+struct ifp_attr_req {
+ const char *name;
+ const char **attrs;
+ int nattrs;
+
+ struct ifp_req *ireq;
+};
+
+static struct tevent_req *
+ifp_user_get_attr_send(TALLOC_CTX *mem_ctx, struct resp_ctx *rctx,
+ struct sss_nc_ctx *ncache, int neg_timeout,
+ const char *inp, const char **attrs);
+static errno_t ifp_user_get_attr_recv(TALLOC_CTX *mem_ctx,
+ struct tevent_req *req,
+ struct ldb_result **_res);
+
+static void ifp_user_get_attr_process(struct tevent_req *req);
+
+static errno_t
+ifp_user_get_attr_handle_reply(struct ifp_req *ireq, const char *user,
+ const char **attrs, struct ldb_result *res);
+static errno_t
+ifp_user_get_attr_unpack_msg(struct ifp_attr_req *attr_req);
+
+int ifp_user_get_attr(struct sbus_request *dbus_req, void *data)
+{
+ errno_t ret;
+ struct ifp_req *ireq;
+ struct ifp_ctx *ifp_ctx;
+ struct ifp_attr_req *attr_req;
+ struct tevent_req *req;
+
+ ifp_ctx = talloc_get_type(data, struct ifp_ctx);
+ if (ifp_ctx == NULL) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "Invalid pointer!\n");
+ return sbus_request_return_and_finish(dbus_req, DBUS_TYPE_INVALID);
+ }
+
+ ret = ifp_req_create(dbus_req, ifp_ctx, &ireq);
+ if (ret != EOK) {
+ return ifp_req_create_handle_failure(dbus_req, ret);
+ }
+
+ attr_req = talloc_zero(ireq, struct ifp_attr_req);
+ if (attr_req == NULL) {
+ return sbus_request_finish(dbus_req, NULL);
+ }
+ attr_req->ireq = ireq;
+
+ ret = ifp_user_get_attr_unpack_msg(attr_req);
+ if (ret != EOK) {
+ return ret; /* handled internally */
+ }
+
+ DEBUG(SSSDBG_FUNC_DATA,
+ "Looking up attributes of user [%s] on behalf of %"PRIi64"\n",
+ attr_req->name, ireq->dbus_req->client);
+
+ req = ifp_user_get_attr_send(ireq, ifp_ctx->rctx,
+ ifp_ctx->ncache, ifp_ctx->neg_timeout,
+ attr_req->name, attr_req->attrs);
+ if (req == NULL) {
+ return sbus_request_finish(dbus_req, NULL);
+ }
+ tevent_req_set_callback(req, ifp_user_get_attr_process, attr_req);
+ return EOK;
+}
+
+static errno_t
+ifp_user_get_attr_unpack_msg(struct ifp_attr_req *attr_req)
+{
+ bool parsed;
+
+ parsed = sbus_request_parse_or_finish(attr_req->ireq->dbus_req,
+ DBUS_TYPE_STRING, &attr_req->name,
+ DBUS_TYPE_ARRAY, DBUS_TYPE_STRING,
+ &attr_req->attrs,
+ &attr_req->nattrs,
+ DBUS_TYPE_INVALID);
+ if (parsed == false) {
+ return EOK; /* handled */
+ }
+
+ return EOK;
+}
+
+static void ifp_user_get_attr_process(struct tevent_req *req)
+{
+ struct ifp_attr_req *attr_req;
+ errno_t ret;
+ struct ldb_result *res = NULL;
+
+ attr_req = tevent_req_callback_data(req, struct ifp_attr_req);
+
+ ret = ifp_user_get_attr_recv(attr_req, req, &res);
+ talloc_zfree(req);
+ if (ret == ENOENT) {
+ sbus_request_fail_and_finish(attr_req->ireq->dbus_req,
+ sbus_error_new(attr_req->ireq->dbus_req,
+ DBUS_ERROR_FAILED,
+ "No such user\n"));
+ return;
+ } else if (ret != EOK) {
+ sbus_request_fail_and_finish(attr_req->ireq->dbus_req,
+ sbus_error_new(attr_req->ireq->dbus_req,
+ DBUS_ERROR_FAILED,
+ "Failed to read user attribute\n"));
+ return;
+ }
+
+ ret = ifp_user_get_attr_handle_reply(attr_req->ireq, attr_req->name,
+ attr_req->attrs, res);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "Could not handle reply!\n");
+ /* Nothing to do, let the client time out */
+ return;
+ }
+}
+
+static errno_t
+ifp_user_get_attr_handle_reply(struct ifp_req *ireq, const char *user,
+ const char **attrs, struct ldb_result *res)
+{
+ errno_t ret;
+ dbus_bool_t dbret;
+ DBusMessage *reply;
+ DBusMessageIter iter;
+ DBusMessageIter iter_dict;
+ struct ldb_message_element *el;
+ int ai;
+
+ /* Construct a reply */
+ reply = dbus_message_new_method_return(ireq->dbus_req->message);
+ if (!reply) {
+ return sbus_request_finish(ireq->dbus_req, NULL);
+ }
+
+ dbus_message_iter_init_append(reply, &iter);
+
+ dbret = dbus_message_iter_open_container(
+ &iter, DBUS_TYPE_ARRAY,
+ DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
+ DBUS_TYPE_STRING_AS_STRING
+ DBUS_TYPE_VARIANT_AS_STRING
+ DBUS_DICT_ENTRY_END_CHAR_AS_STRING,
+ &iter_dict);
+ if (!dbret) {
+ return sbus_request_finish(ireq->dbus_req, NULL);
+ }
+
+ if (res->count > 0) {
+ for (ai = 0; attrs[ai]; ai++) {
+ el = ldb_msg_find_element(res->msgs[0], attrs[ai]);
+ if (el == NULL || el->num_values == 0) {
+ DEBUG(SSSDBG_MINOR_FAILURE,
+ "Attribute %s not present or has no values\n",
+ attrs[ai]);
+ continue;
+ }
+
+ ret = ifp_add_ldb_el_to_dict(&iter_dict, el);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_MINOR_FAILURE,
+ "Cannot add attribute %s to message\n",
+ attrs[ai]);
+ continue;
+ }
+ }
+ }
+
+ dbret = dbus_message_iter_close_container(&iter, &iter_dict);
+ if (!dbret) {
+ return sbus_request_finish(ireq->dbus_req, NULL);
+ }
+
+ return sbus_request_finish(ireq->dbus_req, reply);
+}
+
+struct ifp_user_get_attr_state {
+ const char *inp;
+ const char **attrs;
+ struct ldb_result *res;
+
+ char *name;
+ char *domname;
+
+ struct sss_domain_info *dom;
+ bool check_next;
+ bool check_provider;
+
+ struct resp_ctx *rctx;
+ struct sss_nc_ctx *ncache;
+ int neg_timeout;
+};
+
+static void ifp_user_get_attr_dom(struct tevent_req *subreq);
+static errno_t ifp_user_get_attr_search(struct tevent_req *req);
+int ifp_cache_check(struct ifp_user_get_attr_state *state,
+ sss_dp_callback_t callback,
+ unsigned int cache_refresh_percent,
+ void *pvt);
+void ifp_user_get_attr_done(struct tevent_req *req);
+
+static struct tevent_req *
+ifp_user_get_attr_send(TALLOC_CTX *mem_ctx, struct resp_ctx *rctx,
+ struct sss_nc_ctx *ncache, int neg_timeout,
+ const char *inp, const char **attrs)
+{
+ errno_t ret;
+ struct tevent_req *req;
+ struct tevent_req *subreq;
+ struct ifp_user_get_attr_state *state;
+
+ req = tevent_req_create(mem_ctx, &state, struct ifp_user_get_attr_state);
+ if (req == NULL) {
+ return NULL;
+ }
+ state->inp = inp;
+ state->attrs = attrs;
+ state->rctx = rctx;
+ state->ncache = ncache;
+ state->neg_timeout = neg_timeout;
+
+ subreq = sss_parse_inp_send(req, rctx, inp);
+ if (subreq == NULL) {
+ ret = ENOMEM;
+ goto done;
+ }
+ tevent_req_set_callback(subreq, ifp_user_get_attr_dom, req);
+
+ ret = EOK;
+done:
+ if (ret != EOK) {
+ tevent_req_error(req, ret);
+ }
+ return req;
+}
+
+static void
+ifp_user_get_attr_dom(struct tevent_req *subreq)
+{
+ errno_t ret;
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ struct ifp_user_get_attr_state *state = tevent_req_data(req,
+ struct ifp_user_get_attr_state);
+
+ ret = sss_parse_inp_recv(subreq, state, &state->name, &state->domname);
+ talloc_free(subreq);
+ if (ret != EOK) {
+ tevent_req_error(req, ret);
+ return;
+ }
+
+ if (state->domname) {
+ /* this is a search in one domain */
+ state->dom = responder_get_domain(state->rctx, state->domname);
+ if (state->dom == NULL) {
+ tevent_req_error(req, EINVAL);
+ return;
+ }
+ state->check_next = false;
+ } else {
+ /* this is a multidomain search */
+ state->dom = state->rctx->domains;
+ state->check_next = true;
+ }
+
+ state->check_provider = NEED_CHECK_PROVIDER(state->dom->provider);
+
+ /* All set up, do the search! */
+ ret = ifp_user_get_attr_search(req);
+ if (ret == EOK) {
+ /* The data was cached. Just quit */
+ tevent_req_done(req);
+ return;
+ } else if (ret != EAGAIN) {
+ tevent_req_error(req, ret);
+ return;
+ }
+
+ /* Execution will resume in ifp_dp_callback */
+}
+
+static void ifp_dp_callback(uint16_t err_maj, uint32_t err_min,
+ const char *err_msg, void *ptr);
+
+static errno_t ifp_user_get_attr_search(struct tevent_req *req)
+{
+ struct ifp_user_get_attr_state *state = tevent_req_data(req,
+ struct ifp_user_get_attr_state);
+ struct sss_domain_info *dom = state->dom;
+ char *name = NULL;
+ errno_t ret;
+
+ while (dom) {
+ /* if it is a domainless search, skip domains that require fully
+ * qualified names instead */
+ while (dom && state->check_next && dom->fqnames) {
+ dom = get_next_domain(dom, false);
+ }
+
+ if (!dom) break;
+
+ if (dom != state->dom) {
+ /* make sure we reset the check_provider flag when we check
+ * a new domain */
+ state->check_provider = NEED_CHECK_PROVIDER(dom->provider);
+ }
+
+ /* make sure to update the cache_req if we changed domain */
+ state->dom = dom;
+
+ talloc_free(name);
+ name = sss_get_cased_name(state, state->name, dom->case_sensitive);
+ if (!name) return ENOMEM;
+
+ /* verify this user has not yet been negatively cached,
+ * or has been permanently filtered */
+ ret = sss_ncache_check_user(state->ncache,
+ state->neg_timeout,
+ dom, name);
+ /* if neg cached, return we didn't find it */
+ if (ret == EEXIST) {
+ DEBUG(SSSDBG_TRACE_FUNC,
+ "User [%s] does not exist in [%s]! (negative cache)\n",
+ name, dom->name);
+ /* if a multidomain search, try with next */
+ if (state->check_next) {
+ dom = get_next_domain(dom, false);
+ continue;
+ }
+
+ /* There are no further domains or this was a
+ * fully-qualified user request.
+ */
+ return ENOENT;
+ }
+
+ DEBUG(SSSDBG_FUNC_DATA,
+ "Requesting info for [%s@%s]\n", name, dom->name);
+
+ ret = sysdb_get_user_attr(state, dom, name, state->attrs, &state->res);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "Failed to make request to our cache!\n");
+ return EIO;
+ }
+
+ if (state->res->count > 1) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "getpwnam call returned more than one result !?!\n");
+ return ENOENT;
+ }
+
+ if (state->res->count == 0 && state->check_provider == false) {
+ /* set negative cache only if not result of cache check */
+ ret = sss_ncache_set_user(state->ncache, false, dom, name);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_MINOR_FAILURE, "Cannot set negcache for %s@%s\n",
+ name, dom->name);
+ /* Not fatal */
+ }
+
+ /* if a multidomain search, try with next */
+ if (state->check_next) {
+ dom = get_next_domain(dom, false);
+ if (dom) continue;
+ }
+
+ DEBUG(SSSDBG_TRACE_FUNC, "No results for getpwnam call\n");
+ return ENOENT;
+ }
+
+ /* if this is a caching provider (or if we haven't checked the cache
+ * yet) then verify that the cache is uptodate */
+ if (state->check_provider) {
+ ret = ifp_cache_check(state, ifp_dp_callback, 0, req);
+ if (ret != EOK) {
+ /* Anything but EOK means we should reenter the mainloop
+ * because we may be refreshing the cache
+ */
+ return ret;
+ }
+ }
+
+ /* One result found */
+ DEBUG(SSSDBG_TRACE_FUNC,
+ "Returning info for user [%s@%s]\n", name, dom->name);
+ return EOK;
+ }
+
+ DEBUG(SSSDBG_MINOR_FAILURE,
+ "No matching domain found for [%s], fail!\n", state->inp);
+ return ENOENT;
+}
+
+int ifp_cache_check(struct ifp_user_get_attr_state *state,
+ sss_dp_callback_t callback,
+ unsigned int cache_refresh_percent,
+ void *pvt)
+{
+ uint64_t cache_expire = 0;
+ int ret;
+ struct tevent_req *req;
+ struct dp_callback_ctx *cb_ctx = NULL;
+
+ if (state->res->count > 1) {
+ DEBUG(SSSDBG_OP_FAILURE,
+ "cache search call returned more than one result! "
+ "DB Corrupted?\n");
+ return ENOENT;
+ }
+
+ if (state->res->count > 0) {
+ cache_expire = ldb_msg_find_attr_as_uint64(state->res->msgs[0],
+ SYSDB_CACHE_EXPIRE, 0);
+
+ /* if we have any reply let's check cache validity */
+ ret = sss_cmd_check_cache(state->res->msgs[0], cache_refresh_percent,
+ cache_expire);
+ if (ret == EOK) {
+ DEBUG(SSSDBG_TRACE_FUNC, "Cached entry is valid, returning..\n");
+ return EOK;
+ } else if (ret != EAGAIN && ret != ENOENT) {
+ DEBUG(SSSDBG_CRIT_FAILURE, "Error checking cache: %d\n", ret);
+ return ret;
+ }
+ } else {
+ /* No replies */
+ ret = ENOENT;
+ }
+
+ /* EAGAIN (off band) or ENOENT (cache miss) -> check cache */
+ if (ret == EAGAIN) {
+ /* No callback required
+ * This was an out-of-band update. We'll return EOK
+ * so the calling function can return the cached entry
+ * immediately.
+ */
+ DEBUG(SSSDBG_TRACE_FUNC, "Performing midpoint cache update\n");
+
+ req = sss_dp_get_account_send(state, state->rctx, state->dom, true,
+ SSS_DP_USER, state->inp, 0,
+ NULL);
+ if (req == NULL) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "Out of memory sending out-of-band data provider "
+ "request\n");
+ /* This is non-fatal, so we'll continue here */
+ } else {
+ DEBUG(SSSDBG_TRACE_FUNC, "Updating cache out-of-band\n");
+ }
+
+ /* We don't need to listen for a reply, so we will free the
+ * request here.
+ */
+ talloc_zfree(req);
+ } else {
+ /* This is a cache miss. Or the cache is expired.
+ * We need to get the updated user information before returning it.
+ */
+
+ /* dont loop forever; mark the provider as checked */
+ state->check_provider = false;
+
+ req = sss_dp_get_account_send(state, state->rctx, state->dom, true,
+ SSS_DP_USER, state->inp, 0, NULL);
+ if (req == NULL) {
+ DEBUG(SSSDBG_CRIT_FAILURE,
+ "Out of memory sending data provider request\n");
+ return ENOMEM;
+ }
+
+ cb_ctx = talloc_zero(state, struct dp_callback_ctx);
+ if (cb_ctx == NULL) {
+ talloc_zfree(req);
+ return ENOMEM;
+ }
+ cb_ctx->callback = callback;
+ cb_ctx->ptr = pvt;
+ cb_ctx->cctx = NULL; /* There is no client in ifp */
+ cb_ctx->mem_ctx = state;
+
+ tevent_req_set_callback(req, ifp_user_get_attr_done, cb_ctx);
+ return EAGAIN;
+ }
+
+ return EOK;
+}
+
+void ifp_user_get_attr_done(struct tevent_req *req)
+{
+ struct dp_callback_ctx *cb_ctx =
+ tevent_req_callback_data(req, struct dp_callback_ctx);
+
+ errno_t ret;
+ dbus_uint16_t err_maj;
+ dbus_uint32_t err_min;
+ char *err_msg;
+
+ ret = sss_dp_get_account_recv(cb_ctx->mem_ctx, req,
+ &err_maj, &err_min,
+ &err_msg);
+ talloc_zfree(req);
+ if (ret != EOK) {
+ DEBUG(SSSDBG_OP_FAILURE, "Could not get account info: %d\n", ret);
+ /* report error with callback */
+ }
+
+ cb_ctx->callback(err_maj, err_min, err_msg, cb_ctx->ptr);
+}
+
+static void ifp_dp_callback(uint16_t err_maj, uint32_t err_min,
+ const char *err_msg, void *ptr)
+{
+ errno_t ret;
+ struct tevent_req *req = talloc_get_type(ptr, struct tevent_req);
+
+ if (err_maj) {
+ DEBUG(SSSDBG_MINOR_FAILURE,
+ "Unable to get information from Data Provider\n"
+ "Error: %u, %u, %s\n"
+ "Will try to return what we have in cache\n",
+ (unsigned int)err_maj, (unsigned int)err_min, err_msg);
+ }
+
+ /* Backend was updated successfully. Check again */
+ ret = ifp_user_get_attr_search(req);
+ if (ret == EAGAIN) {
+ /* Another search in progress */
+ return;
+ } else if (ret != EOK) {
+ tevent_req_error(req, ret);
+ return;
+ }
+
+ tevent_req_done(req);
+}
+
+static errno_t
+ifp_user_get_attr_recv(TALLOC_CTX *mem_ctx,
+ struct tevent_req *req,
+ struct ldb_result **_res)
+{
+ struct ifp_user_get_attr_state *state = tevent_req_data(req,
+ struct ifp_user_get_attr_state);
+
+ TEVENT_REQ_RETURN_ON_ERROR(req);
+
+ if (state->res == NULL) {
+ /* Did the request end with success but with no data? */
+ return ENOENT;
+ }
+
+ if (_res) {
+ *_res = talloc_steal(mem_ctx, state->res);
+ }
+ return EOK;
+}
+
struct cli_protocol_version *register_cli_protocol_version(void)
{
static struct cli_protocol_version ssh_cli_protocol_version[] = {