And also ... - Related: upstream#941 - return multiple server addresses to the Kerberos locator plugin - Related: upstream#3652 - kdcinfo doesn't get populated for other domains - Resolves: upstream#3747 - sss_ssh_authorizedkeys exits abruptly if SSHD closes its end of the pipe before reading all the SSH keys - Resolves: upstream#3607 - Handle conflicting e-mail addresses more gracefully - Resolves: upstream#3754 - SSSD AD uses LDAP filter to detect POSIX attributes stored in AD GC also for regular AD DC queries - Related: upstream#3219 - [RFE] Regular expression used in sssd.conf not being able to consume an @-sign in the user/group name. Signed-off-by: Fabiano Fidêncio <fidencio@redhat.com>
494 lines
17 KiB
Diff
494 lines
17 KiB
Diff
From 713bc782502163251ef22eb81b09eed61a8407f7 Mon Sep 17 00:00:00 2001
|
|
From: Sumit Bose <sbose@redhat.com>
|
|
Date: Tue, 5 Jun 2018 17:44:59 +0200
|
|
Subject: [PATCH] krb5: refactor removal of krb5info files
|
|
|
|
Currently a persistent offline callback removes the krb5info files for
|
|
the configured main domain and those files were removed by a SIGTERM
|
|
signal handlers as well.
|
|
|
|
This does not scale if krb5info files are created for sub-domains as
|
|
well. To remove the files automatically the removal is moved into a
|
|
talloc destructor of an offline callback which is added if the file is
|
|
created and frees itself when the system goes offline. Due to the
|
|
talloc memory hierarchy we get removal on shutdown for free.
|
|
|
|
Related to https://pagure.io/SSSD/sssd/issue/3652
|
|
Reviewed-by: Jakub Hrozek <jhrozek@redhat.com>
|
|
|
|
(cherry picked from commit d91661e295c8e878f1bbf34e6f65f61e8301bf0e)
|
|
---
|
|
src/providers/ad/ad_common.c | 7 +-
|
|
src/providers/ipa/ipa_common.c | 5 +-
|
|
src/providers/krb5/krb5_common.c | 176 +++++++++++++-------------
|
|
src/providers/krb5/krb5_common.h | 7 +-
|
|
src/providers/krb5/krb5_init_shared.c | 6 -
|
|
src/providers/ldap/ldap_common.c | 87 -------------
|
|
6 files changed, 102 insertions(+), 186 deletions(-)
|
|
|
|
diff --git a/src/providers/ad/ad_common.c b/src/providers/ad/ad_common.c
|
|
index 0aea985e00faa996643fd7e7630d4264fb6cf233..8caaba6c0d06cfe83d9741536192d662fc936273 100644
|
|
--- a/src/providers/ad/ad_common.c
|
|
+++ b/src/providers/ad/ad_common.c
|
|
@@ -804,6 +804,8 @@ ad_failover_init(TALLOC_CTX *mem_ctx, struct be_ctx *bectx,
|
|
goto done;
|
|
}
|
|
|
|
+ service->krb5_service->be_ctx = bectx;
|
|
+
|
|
if (!primary_servers) {
|
|
DEBUG(SSSDBG_CONF_SETTINGS,
|
|
"No primary servers defined, using service discovery\n");
|
|
@@ -984,8 +986,9 @@ ad_resolve_callback(void *private_data, struct fo_server *server)
|
|
goto done;
|
|
}
|
|
|
|
- ret = write_krb5info_file(service->krb5_service->realm, safe_address,
|
|
- SSS_KRB5KDC_FO_SRV);
|
|
+ ret = write_krb5info_file(service->krb5_service,
|
|
+ safe_address,
|
|
+ SSS_KRB5KDC_FO_SRV);
|
|
if (ret != EOK) {
|
|
DEBUG(SSSDBG_MINOR_FAILURE,
|
|
"write_krb5info_file failed, authentication might fail.\n");
|
|
diff --git a/src/providers/ipa/ipa_common.c b/src/providers/ipa/ipa_common.c
|
|
index 87ed967673358bf833dae13c29b1f6a17b0fc19c..dcbb54a744358718e444972b9827ee64887e5e33 100644
|
|
--- a/src/providers/ipa/ipa_common.c
|
|
+++ b/src/providers/ipa/ipa_common.c
|
|
@@ -838,7 +838,8 @@ static void ipa_resolve_callback(void *private_data, struct fo_server *server)
|
|
return;
|
|
}
|
|
|
|
- ret = write_krb5info_file(service->krb5_service->realm, safe_address,
|
|
+ ret = write_krb5info_file(service->krb5_service,
|
|
+ safe_address,
|
|
SSS_KRB5KDC_FO_SRV);
|
|
if (ret != EOK) {
|
|
DEBUG(SSSDBG_OP_FAILURE,
|
|
@@ -1012,6 +1013,8 @@ int ipa_service_init(TALLOC_CTX *memctx, struct be_ctx *ctx,
|
|
goto done;
|
|
}
|
|
|
|
+ service->krb5_service->be_ctx = ctx;
|
|
+
|
|
if (!primary_servers) {
|
|
DEBUG(SSSDBG_CONF_SETTINGS,
|
|
"No primary servers defined, using service discovery\n");
|
|
diff --git a/src/providers/krb5/krb5_common.c b/src/providers/krb5/krb5_common.c
|
|
index 520e7591ce1b37b4a8dea357b6dd0ec7afd76f58..c6896a6cd663da896075e72aa0a0602c198b45e8 100644
|
|
--- a/src/providers/krb5/krb5_common.c
|
|
+++ b/src/providers/krb5/krb5_common.c
|
|
@@ -389,7 +389,76 @@ done:
|
|
return ret;
|
|
}
|
|
|
|
-errno_t write_krb5info_file(const char *realm, const char *server,
|
|
+static int remove_info_files_destructor(void *p)
|
|
+{
|
|
+ int ret;
|
|
+ struct remove_info_files_ctx *ctx = talloc_get_type(p,
|
|
+ struct remove_info_files_ctx);
|
|
+
|
|
+ ret = remove_krb5_info_files(ctx, ctx->realm);
|
|
+ if (ret != EOK) {
|
|
+ DEBUG(SSSDBG_CRIT_FAILURE, "remove_krb5_info_files failed.\n");
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static errno_t
|
|
+krb5_add_krb5info_offline_callback(struct krb5_service *krb5_service)
|
|
+{
|
|
+ int ret;
|
|
+ struct remove_info_files_ctx *ctx;
|
|
+
|
|
+ if (krb5_service == NULL || krb5_service->name == NULL
|
|
+ || krb5_service->realm == NULL
|
|
+ || krb5_service->be_ctx == NULL) {
|
|
+ DEBUG(SSSDBG_CRIT_FAILURE, "Missing KDC service name or realm!\n");
|
|
+ return EINVAL;
|
|
+ }
|
|
+
|
|
+ ctx = talloc_zero(krb5_service->be_ctx, struct remove_info_files_ctx);
|
|
+ if (ctx == NULL) {
|
|
+ DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zfree failed.\n");
|
|
+ return ENOMEM;
|
|
+ }
|
|
+
|
|
+ ctx->realm = talloc_strdup(ctx, krb5_service->realm);
|
|
+ if (ctx->realm == NULL) {
|
|
+ DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed!\n");
|
|
+ ret = ENOMEM;
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ ctx->be_ctx = krb5_service->be_ctx;
|
|
+ ctx->kdc_service_name = talloc_strdup(ctx, krb5_service->name);
|
|
+ if (ctx->kdc_service_name == NULL) {
|
|
+ DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed!\n");
|
|
+ ret = ENOMEM;
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ ret = be_add_offline_cb(ctx, krb5_service->be_ctx,
|
|
+ remove_krb5_info_files_callback, ctx, NULL);
|
|
+ if (ret != EOK) {
|
|
+ DEBUG(SSSDBG_CRIT_FAILURE, "be_add_offline_cb failed.\n");
|
|
+ goto done;
|
|
+ }
|
|
+
|
|
+ talloc_set_destructor((TALLOC_CTX *) ctx, remove_info_files_destructor);
|
|
+
|
|
+ ret = EOK;
|
|
+
|
|
+done:
|
|
+ if (ret != EOK) {
|
|
+ talloc_zfree(ctx);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+
|
|
+errno_t write_krb5info_file(struct krb5_service *krb5_service,
|
|
+ const char *server,
|
|
const char *service)
|
|
{
|
|
int ret;
|
|
@@ -401,17 +470,19 @@ errno_t write_krb5info_file(const char *realm, const char *server,
|
|
size_t server_len;
|
|
ssize_t written;
|
|
|
|
- if (realm == NULL || *realm == '\0' || server == NULL || *server == '\0' ||
|
|
- service == NULL || *service == '\0') {
|
|
+ if (krb5_service == NULL || krb5_service->realm == NULL
|
|
+ || *krb5_service->realm == '\0'
|
|
+ || server == NULL || *server == '\0'
|
|
+ || service == NULL || *service == '\0') {
|
|
DEBUG(SSSDBG_CRIT_FAILURE,
|
|
"Missing or empty realm, server or service.\n");
|
|
return EINVAL;
|
|
}
|
|
|
|
- if (sss_krb5_realm_has_proxy(realm)) {
|
|
+ if (sss_krb5_realm_has_proxy(krb5_service->realm)) {
|
|
DEBUG(SSSDBG_CONF_SETTINGS,
|
|
"KDC Proxy available for realm [%s], no kdcinfo file created.\n",
|
|
- realm);
|
|
+ krb5_service->realm);
|
|
return EOK;
|
|
}
|
|
|
|
@@ -439,7 +510,7 @@ errno_t write_krb5info_file(const char *realm, const char *server,
|
|
goto done;
|
|
}
|
|
|
|
- krb5info_name = talloc_asprintf(tmp_ctx, name_tmpl, realm);
|
|
+ krb5info_name = talloc_asprintf(tmp_ctx, name_tmpl, krb5_service->realm);
|
|
if (krb5info_name == NULL) {
|
|
DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n");
|
|
ret = ENOMEM;
|
|
@@ -495,6 +566,12 @@ errno_t write_krb5info_file(const char *realm, const char *server,
|
|
goto done;
|
|
}
|
|
|
|
+ ret = krb5_add_krb5info_offline_callback(krb5_service);
|
|
+ if (ret != EOK) {
|
|
+ DEBUG(SSSDBG_OP_FAILURE, "Failed to add offline callback, krb5info "
|
|
+ "file might not be removed properly.\n");
|
|
+ }
|
|
+
|
|
ret = EOK;
|
|
done:
|
|
if (fd != -1) {
|
|
@@ -561,7 +638,8 @@ static void krb5_resolve_callback(void *private_data, struct fo_server *server)
|
|
return;
|
|
}
|
|
|
|
- ret = write_krb5info_file(krb5_service->realm, safe_address,
|
|
+ ret = write_krb5info_file(krb5_service,
|
|
+ safe_address,
|
|
krb5_service->name);
|
|
if (ret != EOK) {
|
|
DEBUG(SSSDBG_OP_FAILURE,
|
|
@@ -761,6 +839,7 @@ int krb5_service_init(TALLOC_CTX *memctx, struct be_ctx *ctx,
|
|
}
|
|
|
|
service->write_kdcinfo = use_kdcinfo;
|
|
+ service->be_ctx = ctx;
|
|
|
|
if (!primary_servers) {
|
|
DEBUG(SSSDBG_CONF_SETTINGS,
|
|
@@ -839,7 +918,6 @@ errno_t remove_krb5_info_files(TALLOC_CTX *mem_ctx, const char *realm)
|
|
void remove_krb5_info_files_callback(void *pvt)
|
|
{
|
|
int ret;
|
|
- TALLOC_CTX *tmp_ctx = NULL;
|
|
struct remove_info_files_ctx *ctx = talloc_get_type(pvt,
|
|
struct remove_info_files_ctx);
|
|
|
|
@@ -864,19 +942,10 @@ void remove_krb5_info_files_callback(void *pvt)
|
|
}
|
|
}
|
|
|
|
- tmp_ctx = talloc_new(NULL);
|
|
- if (tmp_ctx == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE,
|
|
- "talloc_new failed, cannot remove krb5 info files.\n");
|
|
- return;
|
|
- }
|
|
-
|
|
- ret = remove_krb5_info_files(tmp_ctx, ctx->realm);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "remove_krb5_info_files failed.\n");
|
|
- }
|
|
-
|
|
- talloc_zfree(tmp_ctx);
|
|
+ /* Freeing the remove_info_files_ctx will remove the related krb5info
|
|
+ * file. Additionally the callback from the list of callbacks is removed,
|
|
+ * it will be added again when a new krb5info file is created. */
|
|
+ talloc_free(ctx);
|
|
}
|
|
|
|
void krb5_finalize(struct tevent_context *ev,
|
|
@@ -886,74 +955,9 @@ void krb5_finalize(struct tevent_context *ev,
|
|
void *siginfo,
|
|
void *private_data)
|
|
{
|
|
- char *realm = (char *)private_data;
|
|
- int ret;
|
|
-
|
|
- ret = remove_krb5_info_files(se, realm);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "remove_krb5_info_files failed.\n");
|
|
- }
|
|
-
|
|
orderly_shutdown(0);
|
|
}
|
|
|
|
-errno_t krb5_install_offline_callback(struct be_ctx *be_ctx,
|
|
- struct krb5_ctx *krb5_ctx)
|
|
-{
|
|
- int ret;
|
|
- struct remove_info_files_ctx *ctx;
|
|
- const char *krb5_realm;
|
|
-
|
|
- if (krb5_ctx->service == NULL || krb5_ctx->service->name == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "Missing KDC service name!\n");
|
|
- return EINVAL;
|
|
- }
|
|
-
|
|
- ctx = talloc_zero(krb5_ctx, struct remove_info_files_ctx);
|
|
- if (ctx == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zfree failed.\n");
|
|
- return ENOMEM;
|
|
- }
|
|
-
|
|
- krb5_realm = dp_opt_get_cstring(krb5_ctx->opts, KRB5_REALM);
|
|
- if (krb5_realm == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "Missing krb5_realm option!\n");
|
|
- ret = EINVAL;
|
|
- goto done;
|
|
- }
|
|
-
|
|
- ctx->realm = talloc_strdup(ctx, krb5_realm);
|
|
- if (ctx->realm == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed!\n");
|
|
- ret = ENOMEM;
|
|
- goto done;
|
|
- }
|
|
-
|
|
- ctx->be_ctx = be_ctx;
|
|
- ctx->kdc_service_name = krb5_ctx->service->name;
|
|
- if (krb5_ctx->kpasswd_service == NULL) {
|
|
- ctx->kpasswd_service_name =NULL;
|
|
- } else {
|
|
- ctx->kpasswd_service_name = krb5_ctx->kpasswd_service->name;
|
|
- }
|
|
-
|
|
- ret = be_add_offline_cb(ctx, be_ctx, remove_krb5_info_files_callback, ctx,
|
|
- NULL);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "be_add_offline_cb failed.\n");
|
|
- goto done;
|
|
- }
|
|
-
|
|
- ret = EOK;
|
|
-
|
|
-done:
|
|
- if (ret != EOK) {
|
|
- talloc_zfree(ctx);
|
|
- }
|
|
-
|
|
- return ret;
|
|
-}
|
|
-
|
|
errno_t krb5_install_sigterm_handler(struct tevent_context *ev,
|
|
struct krb5_ctx *krb5_ctx)
|
|
{
|
|
diff --git a/src/providers/krb5/krb5_common.h b/src/providers/krb5/krb5_common.h
|
|
index 48368a528e75947102c74cb75bf7a74ec0dd258f..a2e47b0605debdffa28305dab4f7674707f713ac 100644
|
|
--- a/src/providers/krb5/krb5_common.h
|
|
+++ b/src/providers/krb5/krb5_common.h
|
|
@@ -67,6 +67,7 @@ enum krb5_opts {
|
|
typedef enum { INIT_PW, INIT_KT, RENEW, VALIDATE } action_type;
|
|
|
|
struct krb5_service {
|
|
+ struct be_ctx *be_ctx;
|
|
char *name;
|
|
char *realm;
|
|
bool write_kdcinfo;
|
|
@@ -157,7 +158,8 @@ errno_t krb5_try_kdcip(struct confdb_ctx *cdb, const char *conf_path,
|
|
errno_t sss_krb5_get_options(TALLOC_CTX *memctx, struct confdb_ctx *cdb,
|
|
const char *conf_path, struct dp_option **_opts);
|
|
|
|
-errno_t write_krb5info_file(const char *realm, const char *kdc,
|
|
+errno_t write_krb5info_file(struct krb5_service *krb5_service,
|
|
+ const char *server,
|
|
const char *service);
|
|
|
|
int krb5_service_init(TALLOC_CTX *memctx, struct be_ctx *ctx,
|
|
@@ -177,9 +179,6 @@ void krb5_finalize(struct tevent_context *ev,
|
|
void *siginfo,
|
|
void *private_data);
|
|
|
|
-errno_t krb5_install_offline_callback(struct be_ctx *be_ctx,
|
|
- struct krb5_ctx *krb_ctx);
|
|
-
|
|
errno_t krb5_install_sigterm_handler(struct tevent_context *ev,
|
|
struct krb5_ctx *krb5_ctx);
|
|
|
|
diff --git a/src/providers/krb5/krb5_init_shared.c b/src/providers/krb5/krb5_init_shared.c
|
|
index 3901b7272119c32930c2b6b47279a2c685bf3cfb..368d6f7b0f2bc038e4cc4aa8f0970cd0e81d7b6b 100644
|
|
--- a/src/providers/krb5/krb5_init_shared.c
|
|
+++ b/src/providers/krb5/krb5_init_shared.c
|
|
@@ -71,12 +71,6 @@ errno_t krb5_child_init(struct krb5_ctx *krb5_auth_ctx,
|
|
goto done;
|
|
}
|
|
|
|
- ret = krb5_install_offline_callback(bectx, krb5_auth_ctx);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "krb5_install_offline_callback failed.\n");
|
|
- goto done;
|
|
- }
|
|
-
|
|
ret = krb5_install_sigterm_handler(bectx->ev, krb5_auth_ctx);
|
|
if (ret != EOK) {
|
|
DEBUG(SSSDBG_CRIT_FAILURE, "krb5_install_sigterm_handler failed.\n");
|
|
diff --git a/src/providers/ldap/ldap_common.c b/src/providers/ldap/ldap_common.c
|
|
index 91e229243b9a1b43e7a57704824f5db0341f4ee9..15377ee1f062c0167aabee30ef0757ebe7271682 100644
|
|
--- a/src/providers/ldap/ldap_common.c
|
|
+++ b/src/providers/ldap/ldap_common.c
|
|
@@ -158,14 +158,6 @@ static void sdap_finalize(struct tevent_context *ev,
|
|
void *siginfo,
|
|
void *private_data)
|
|
{
|
|
- char *realm = (char *) private_data;
|
|
- int ret;
|
|
-
|
|
- ret = remove_krb5_info_files(se, realm);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "remove_krb5_info_files failed.\n");
|
|
- }
|
|
-
|
|
orderly_shutdown(0);
|
|
}
|
|
|
|
@@ -196,78 +188,6 @@ errno_t sdap_install_sigterm_handler(TALLOC_CTX *mem_ctx,
|
|
return EOK;
|
|
}
|
|
|
|
-void sdap_remove_kdcinfo_files_callback(void *pvt)
|
|
-{
|
|
- int ret;
|
|
- TALLOC_CTX *tmp_ctx = NULL;
|
|
- struct remove_info_files_ctx *ctx = talloc_get_type(pvt,
|
|
- struct remove_info_files_ctx);
|
|
-
|
|
- ret = be_fo_run_callbacks_at_next_request(ctx->be_ctx,
|
|
- ctx->kdc_service_name);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE,
|
|
- "be_fo_run_callbacks_at_next_request failed, "
|
|
- "krb5 info files will not be removed, because "
|
|
- "it is unclear if they will be recreated properly.\n");
|
|
- return;
|
|
- }
|
|
-
|
|
- tmp_ctx = talloc_new(NULL);
|
|
- if (tmp_ctx == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE,
|
|
- "talloc_new failed, cannot remove krb5 info files.\n");
|
|
- return;
|
|
- }
|
|
-
|
|
- ret = remove_krb5_info_files(tmp_ctx, ctx->realm);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "remove_krb5_info_files failed.\n");
|
|
- }
|
|
-
|
|
- talloc_zfree(tmp_ctx);
|
|
-}
|
|
-
|
|
-
|
|
-errno_t sdap_install_offline_callback(TALLOC_CTX *mem_ctx,
|
|
- struct be_ctx *be_ctx,
|
|
- const char *realm,
|
|
- const char *service_name)
|
|
-{
|
|
- int ret;
|
|
- struct remove_info_files_ctx *ctx;
|
|
-
|
|
- ctx = talloc_zero(mem_ctx, struct remove_info_files_ctx);
|
|
- if (ctx == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "talloc_zfree failed.\n");
|
|
- return ENOMEM;
|
|
- }
|
|
-
|
|
- ctx->be_ctx = be_ctx;
|
|
- ctx->realm = talloc_strdup(ctx, realm);
|
|
- ctx->kdc_service_name = talloc_strdup(ctx, service_name);
|
|
- if (ctx->realm == NULL || ctx->kdc_service_name == NULL) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed!\n");
|
|
- ret = ENOMEM;
|
|
- goto done;
|
|
- }
|
|
-
|
|
- ret = be_add_offline_cb(ctx, be_ctx,
|
|
- sdap_remove_kdcinfo_files_callback,
|
|
- ctx, NULL);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_CRIT_FAILURE, "be_add_offline_cb failed.\n");
|
|
- goto done;
|
|
- }
|
|
-
|
|
- ret = EOK;
|
|
-done:
|
|
- if (ret != EOK) {
|
|
- talloc_zfree(ctx);
|
|
- }
|
|
- return ret;
|
|
-}
|
|
-
|
|
errno_t
|
|
sdap_set_sasl_options(struct sdap_options *id_opts,
|
|
char *default_primary,
|
|
@@ -458,13 +378,6 @@ int sdap_gssapi_init(TALLOC_CTX *mem_ctx,
|
|
goto done;
|
|
}
|
|
|
|
- ret = sdap_install_offline_callback(mem_ctx, bectx,
|
|
- krb5_realm, SSS_KRB5KDC_FO_SRV);
|
|
- if (ret != EOK) {
|
|
- DEBUG(SSSDBG_FATAL_FAILURE, "Failed to install sigterm handler\n");
|
|
- goto done;
|
|
- }
|
|
-
|
|
sdap_service->kinit_service_name = talloc_strdup(sdap_service,
|
|
service->name);
|
|
if (sdap_service->kinit_service_name == NULL) {
|
|
--
|
|
2.17.1
|
|
|