|
|
48b328 |
From 54c3d176326f719ffefded17bb797bc9e6c7f3c0 Mon Sep 17 00:00:00 2001
|
|
|
48b328 |
From: Sumit Bose <sbose@redhat.com>
|
|
|
48b328 |
Date: Mon, 4 Jun 2018 10:49:33 +0200
|
|
|
48b328 |
Subject: [PATCH 18/23] update: allow to add service names
|
|
|
48b328 |
|
|
|
48b328 |
Related to https://bugzilla.redhat.com/show_bug.cgi?id=1547013
|
|
|
48b328 |
https://bugzilla.redhat.com/show_bug.cgi?id=1545568
|
|
|
48b328 |
---
|
|
|
48b328 |
library/adenroll.c | 136 +++++++++++++++++++++++++++++++++-------------------
|
|
|
48b328 |
library/adkrb5.c | 113 +++++++++++++++++++++++++++++++++++++++++++
|
|
|
48b328 |
library/adprivate.h | 6 +++
|
|
|
48b328 |
3 files changed, 206 insertions(+), 49 deletions(-)
|
|
|
48b328 |
|
|
|
48b328 |
diff --git a/library/adenroll.c b/library/adenroll.c
|
|
|
48b328 |
index ee845ef..6fdc773 100644
|
|
|
48b328 |
--- a/library/adenroll.c
|
|
|
48b328 |
+++ b/library/adenroll.c
|
|
|
48b328 |
@@ -305,13 +305,37 @@ ensure_service_names (adcli_result res,
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
static adcli_result
|
|
|
48b328 |
-ensure_service_principals (adcli_result res,
|
|
|
48b328 |
- adcli_enroll *enroll)
|
|
|
48b328 |
+add_service_names_to_service_principals (adcli_enroll *enroll)
|
|
|
48b328 |
{
|
|
|
48b328 |
char *name;
|
|
|
48b328 |
int length = 0;
|
|
|
48b328 |
int i;
|
|
|
48b328 |
|
|
|
48b328 |
+ if (enroll->service_principals != NULL) {
|
|
|
48b328 |
+ length = seq_count (enroll->service_principals);
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+ for (i = 0; enroll->service_names[i] != NULL; i++) {
|
|
|
48b328 |
+ if (asprintf (&name, "%s/%s", enroll->service_names[i], enroll->computer_name) < 0)
|
|
|
48b328 |
+ return_unexpected_if_reached ();
|
|
|
48b328 |
+ enroll->service_principals = _adcli_strv_add (enroll->service_principals,
|
|
|
48b328 |
+ name, &length);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ if (enroll->host_fqdn) {
|
|
|
48b328 |
+ if (asprintf (&name, "%s/%s", enroll->service_names[i], enroll->host_fqdn) < 0)
|
|
|
48b328 |
+ return_unexpected_if_reached ();
|
|
|
48b328 |
+ enroll->service_principals = _adcli_strv_add (enroll->service_principals,
|
|
|
48b328 |
+ name, &length);
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+ return ADCLI_SUCCESS;
|
|
|
48b328 |
+}
|
|
|
48b328 |
+
|
|
|
48b328 |
+static adcli_result
|
|
|
48b328 |
+ensure_service_principals (adcli_result res,
|
|
|
48b328 |
+ adcli_enroll *enroll)
|
|
|
48b328 |
+{
|
|
|
48b328 |
if (res != ADCLI_SUCCESS)
|
|
|
48b328 |
return res;
|
|
|
48b328 |
|
|
|
48b328 |
@@ -319,20 +343,7 @@ ensure_service_principals (adcli_result res,
|
|
|
48b328 |
|
|
|
48b328 |
if (!enroll->service_principals) {
|
|
|
48b328 |
assert (enroll->service_names != NULL);
|
|
|
48b328 |
-
|
|
|
48b328 |
- for (i = 0; enroll->service_names[i] != NULL; i++) {
|
|
|
48b328 |
- if (asprintf (&name, "%s/%s", enroll->service_names[i], enroll->computer_name) < 0)
|
|
|
48b328 |
- return_unexpected_if_reached ();
|
|
|
48b328 |
- enroll->service_principals = _adcli_strv_add (enroll->service_principals,
|
|
|
48b328 |
- name, &length);
|
|
|
48b328 |
-
|
|
|
48b328 |
- if (enroll->host_fqdn) {
|
|
|
48b328 |
- if (asprintf (&name, "%s/%s", enroll->service_names[i], enroll->host_fqdn) < 0)
|
|
|
48b328 |
- return_unexpected_if_reached ();
|
|
|
48b328 |
- enroll->service_principals = _adcli_strv_add (enroll->service_principals,
|
|
|
48b328 |
- name, &length);
|
|
|
48b328 |
- }
|
|
|
48b328 |
- }
|
|
|
48b328 |
+ return add_service_names_to_service_principals (enroll);
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
return ADCLI_SUCCESS;
|
|
|
48b328 |
@@ -356,6 +367,7 @@ ensure_keytab_principals (adcli_result res,
|
|
|
48b328 |
return_unexpected_if_fail (k5 != NULL);
|
|
|
48b328 |
|
|
|
48b328 |
enroll->keytab_principals = calloc (count + 3, sizeof (krb5_principal));
|
|
|
48b328 |
+ return_unexpected_if_fail (enroll->keytab_principals != NULL);
|
|
|
48b328 |
at = 0;
|
|
|
48b328 |
|
|
|
48b328 |
/* First add the principal for the computer account name */
|
|
|
48b328 |
@@ -1266,7 +1278,7 @@ update_computer_account (adcli_enroll *enroll)
|
|
|
48b328 |
}
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
- if (res == ADCLI_SUCCESS && !enroll->user_princpal_generate) {
|
|
|
48b328 |
+ if (res == ADCLI_SUCCESS && enroll->user_principal != NULL && !enroll->user_princpal_generate) {
|
|
|
48b328 |
char *vals_userPrincipalName[] = { enroll->user_principal, NULL };
|
|
|
48b328 |
LDAPMod userPrincipalName = { LDAP_MOD_REPLACE, "userPrincipalName", { vals_userPrincipalName, }, };
|
|
|
48b328 |
LDAPMod *mods[] = { &userPrincipalName, NULL, };
|
|
|
48b328 |
@@ -1519,7 +1531,8 @@ add_principal_to_keytab (adcli_enroll *enroll,
|
|
|
48b328 |
krb5_context k5,
|
|
|
48b328 |
krb5_principal principal,
|
|
|
48b328 |
const char *principal_name,
|
|
|
48b328 |
- int *which_salt)
|
|
|
48b328 |
+ int *which_salt,
|
|
|
48b328 |
+ adcli_enroll_flags flags)
|
|
|
48b328 |
{
|
|
|
48b328 |
match_principal_kvno closure;
|
|
|
48b328 |
krb5_data password;
|
|
|
48b328 |
@@ -1547,41 +1560,47 @@ add_principal_to_keytab (adcli_enroll *enroll,
|
|
|
48b328 |
enroll->keytab_name);
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
- password.data = enroll->computer_password;
|
|
|
48b328 |
- password.length = strlen (enroll->computer_password);
|
|
|
48b328 |
-
|
|
|
48b328 |
enctypes = adcli_enroll_get_keytab_enctypes (enroll);
|
|
|
48b328 |
|
|
|
48b328 |
- /*
|
|
|
48b328 |
- * So we need to discover which salt to use. As a side effect we are
|
|
|
48b328 |
- * also testing that our account works.
|
|
|
48b328 |
- */
|
|
|
48b328 |
+ if (flags & ADCLI_ENROLL_PASSWORD_VALID) {
|
|
|
48b328 |
+ code = _adcli_krb5_keytab_copy_entries (k5, enroll->keytab, principal,
|
|
|
48b328 |
+ enroll->kvno, enctypes);
|
|
|
48b328 |
+ } else {
|
|
|
48b328 |
|
|
|
48b328 |
- salts = build_principal_salts (enroll, k5, principal);
|
|
|
48b328 |
- return_unexpected_if_fail (salts != NULL);
|
|
|
48b328 |
+ password.data = enroll->computer_password;
|
|
|
48b328 |
+ password.length = strlen (enroll->computer_password);
|
|
|
48b328 |
|
|
|
48b328 |
- if (*which_salt < 0) {
|
|
|
48b328 |
- code = _adcli_krb5_keytab_discover_salt (k5, principal, enroll->kvno, &password,
|
|
|
48b328 |
- enctypes, salts, which_salt);
|
|
|
48b328 |
- if (code != 0) {
|
|
|
48b328 |
- _adcli_warn ("Couldn't authenticate with keytab while discovering which salt to use: %s: %s",
|
|
|
48b328 |
- principal_name, krb5_get_error_message (k5, code));
|
|
|
48b328 |
- *which_salt = DEFAULT_SALT;
|
|
|
48b328 |
- } else {
|
|
|
48b328 |
- assert (*which_salt >= 0);
|
|
|
48b328 |
- _adcli_info ("Discovered which keytab salt to use");
|
|
|
48b328 |
+ /*
|
|
|
48b328 |
+ * So we need to discover which salt to use. As a side effect we are
|
|
|
48b328 |
+ * also testing that our account works.
|
|
|
48b328 |
+ */
|
|
|
48b328 |
+
|
|
|
48b328 |
+ salts = build_principal_salts (enroll, k5, principal);
|
|
|
48b328 |
+ return_unexpected_if_fail (salts != NULL);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ if (*which_salt < 0) {
|
|
|
48b328 |
+ code = _adcli_krb5_keytab_discover_salt (k5, principal, enroll->kvno, &password,
|
|
|
48b328 |
+ enctypes, salts, which_salt);
|
|
|
48b328 |
+ if (code != 0) {
|
|
|
48b328 |
+ _adcli_warn ("Couldn't authenticate with keytab while discovering which salt to use: %s: %s",
|
|
|
48b328 |
+ principal_name, krb5_get_error_message (k5, code));
|
|
|
48b328 |
+ *which_salt = DEFAULT_SALT;
|
|
|
48b328 |
+ } else {
|
|
|
48b328 |
+ assert (*which_salt >= 0);
|
|
|
48b328 |
+ _adcli_info ("Discovered which keytab salt to use");
|
|
|
48b328 |
+ }
|
|
|
48b328 |
}
|
|
|
48b328 |
- }
|
|
|
48b328 |
|
|
|
48b328 |
- code = _adcli_krb5_keytab_add_entries (k5, enroll->keytab, principal,
|
|
|
48b328 |
- enroll->kvno, &password, enctypes, &salts[*which_salt]);
|
|
|
48b328 |
+ code = _adcli_krb5_keytab_add_entries (k5, enroll->keytab, principal,
|
|
|
48b328 |
+ enroll->kvno, &password, enctypes, &salts[*which_salt]);
|
|
|
48b328 |
|
|
|
48b328 |
- free_principal_salts (k5, salts);
|
|
|
48b328 |
+ free_principal_salts (k5, salts);
|
|
|
48b328 |
|
|
|
48b328 |
- if (code != 0) {
|
|
|
48b328 |
- _adcli_err ("Couldn't add keytab entries: %s: %s",
|
|
|
48b328 |
- enroll->keytab_name, krb5_get_error_message (k5, code));
|
|
|
48b328 |
- return ADCLI_ERR_FAIL;
|
|
|
48b328 |
+ if (code != 0) {
|
|
|
48b328 |
+ _adcli_err ("Couldn't add keytab entries: %s: %s",
|
|
|
48b328 |
+ enroll->keytab_name, krb5_get_error_message (k5, code));
|
|
|
48b328 |
+ return ADCLI_ERR_FAIL;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
|
|
|
48b328 |
@@ -1591,7 +1610,8 @@ add_principal_to_keytab (adcli_enroll *enroll,
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
static adcli_result
|
|
|
48b328 |
-update_keytab_for_principals (adcli_enroll *enroll)
|
|
|
48b328 |
+update_keytab_for_principals (adcli_enroll *enroll,
|
|
|
48b328 |
+ adcli_enroll_flags flags)
|
|
|
48b328 |
{
|
|
|
48b328 |
krb5_context k5;
|
|
|
48b328 |
adcli_result res;
|
|
|
48b328 |
@@ -1608,7 +1628,7 @@ update_keytab_for_principals (adcli_enroll *enroll)
|
|
|
48b328 |
if (krb5_unparse_name (k5, enroll->keytab_principals[i], &name) != 0)
|
|
|
48b328 |
name = "";
|
|
|
48b328 |
res = add_principal_to_keytab (enroll, k5, enroll->keytab_principals[i],
|
|
|
48b328 |
- name, &which_salt);
|
|
|
48b328 |
+ name, &which_salt, flags);
|
|
|
48b328 |
krb5_free_unparsed_name (k5, name);
|
|
|
48b328 |
|
|
|
48b328 |
if (res != ADCLI_SUCCESS)
|
|
|
48b328 |
@@ -1807,6 +1827,20 @@ enroll_join_or_update_tasks (adcli_enroll *enroll,
|
|
|
48b328 |
/* We ignore failures of setting these fields */
|
|
|
48b328 |
update_and_calculate_enctypes (enroll);
|
|
|
48b328 |
update_computer_account (enroll);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ /* service_names is only set from input on the command line, so no
|
|
|
48b328 |
+ * additional check for explicit is needed here */
|
|
|
48b328 |
+ if (enroll->service_names != NULL) {
|
|
|
48b328 |
+ res = add_service_names_to_service_principals (enroll);
|
|
|
48b328 |
+ if (res != ADCLI_SUCCESS) {
|
|
|
48b328 |
+ return res;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+ res = ensure_keytab_principals (res, enroll);
|
|
|
48b328 |
+ if (res != ADCLI_SUCCESS) {
|
|
|
48b328 |
+ return res;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
update_service_principals (enroll);
|
|
|
48b328 |
|
|
|
48b328 |
if ( (flags & ADCLI_ENROLL_ADD_SAMBA_DATA) && ! (flags & ADCLI_ENROLL_PASSWORD_VALID)) {
|
|
|
48b328 |
@@ -1826,7 +1860,7 @@ enroll_join_or_update_tasks (adcli_enroll *enroll,
|
|
|
48b328 |
* that we use for salting.
|
|
|
48b328 |
*/
|
|
|
48b328 |
|
|
|
48b328 |
- return update_keytab_for_principals (enroll);
|
|
|
48b328 |
+ return update_keytab_for_principals (enroll, flags);
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
adcli_result
|
|
|
48b328 |
@@ -1927,7 +1961,11 @@ adcli_enroll_update (adcli_enroll *enroll,
|
|
|
48b328 |
|
|
|
48b328 |
if (_adcli_check_nt_time_string_lifetime (value,
|
|
|
48b328 |
adcli_enroll_get_computer_password_lifetime (enroll))) {
|
|
|
48b328 |
- flags |= ADCLI_ENROLL_NO_KEYTAB;
|
|
|
48b328 |
+ /* Do not update keytab if neither new service principals have
|
|
|
48b328 |
+ * to be added nor the user principal has to be changed. */
|
|
|
48b328 |
+ if (enroll->service_names == NULL && (enroll->user_principal == NULL || enroll->user_princpal_generate)) {
|
|
|
48b328 |
+ flags |= ADCLI_ENROLL_NO_KEYTAB;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
flags |= ADCLI_ENROLL_PASSWORD_VALID;
|
|
|
48b328 |
}
|
|
|
48b328 |
free (value);
|
|
|
48b328 |
diff --git a/library/adkrb5.c b/library/adkrb5.c
|
|
|
48b328 |
index b0e903e..033c181 100644
|
|
|
48b328 |
--- a/library/adkrb5.c
|
|
|
48b328 |
+++ b/library/adkrb5.c
|
|
|
48b328 |
@@ -204,6 +204,119 @@ _adcli_krb5_open_keytab (krb5_context k5,
|
|
|
48b328 |
return ADCLI_SUCCESS;
|
|
|
48b328 |
}
|
|
|
48b328 |
|
|
|
48b328 |
+typedef struct {
|
|
|
48b328 |
+ krb5_kvno kvno;
|
|
|
48b328 |
+ krb5_enctype enctype;
|
|
|
48b328 |
+ int matched;
|
|
|
48b328 |
+} match_enctype_kvno;
|
|
|
48b328 |
+
|
|
|
48b328 |
+static krb5_boolean
|
|
|
48b328 |
+match_enctype_and_kvno (krb5_context k5,
|
|
|
48b328 |
+ krb5_keytab_entry *entry,
|
|
|
48b328 |
+ void *data)
|
|
|
48b328 |
+{
|
|
|
48b328 |
+ krb5_boolean similar = FALSE;
|
|
|
48b328 |
+ match_enctype_kvno *closure = data;
|
|
|
48b328 |
+ krb5_error_code code;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ assert (closure->enctype);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ code = krb5_c_enctype_compare (k5, closure->enctype, entry->key.enctype,
|
|
|
48b328 |
+ &similar);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ if (code == 0 && entry->vno == closure->kvno && similar) {
|
|
|
48b328 |
+ closure->matched = 1;
|
|
|
48b328 |
+ return 1;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+ return 0;
|
|
|
48b328 |
+}
|
|
|
48b328 |
+
|
|
|
48b328 |
+static krb5_error_code
|
|
|
48b328 |
+_adcli_krb5_get_keyblock (krb5_context k5,
|
|
|
48b328 |
+ krb5_keytab keytab,
|
|
|
48b328 |
+ krb5_keyblock *keyblock,
|
|
|
48b328 |
+ krb5_boolean (* match_func) (krb5_context,
|
|
|
48b328 |
+ krb5_keytab_entry *,
|
|
|
48b328 |
+ void *),
|
|
|
48b328 |
+ void *match_data)
|
|
|
48b328 |
+{
|
|
|
48b328 |
+ krb5_kt_cursor cursor;
|
|
|
48b328 |
+ krb5_keytab_entry entry;
|
|
|
48b328 |
+ krb5_error_code code;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ code = krb5_kt_start_seq_get (k5, keytab, &cursor);
|
|
|
48b328 |
+ if (code == KRB5_KT_END || code == ENOENT)
|
|
|
48b328 |
+ return 0;
|
|
|
48b328 |
+ else if (code != 0)
|
|
|
48b328 |
+ return code;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ for (;;) {
|
|
|
48b328 |
+ code = krb5_kt_next_entry (k5, keytab, &entry, &cursor);
|
|
|
48b328 |
+ if (code != 0)
|
|
|
48b328 |
+ break;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ /* See if we should remove this entry */
|
|
|
48b328 |
+ if (!match_func (k5, &entry, match_data)) {
|
|
|
48b328 |
+ krb5_free_keytab_entry_contents (k5, &entry);
|
|
|
48b328 |
+ continue;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+ code = krb5_copy_keyblock_contents (k5, &entry.key, keyblock);
|
|
|
48b328 |
+ krb5_free_keytab_entry_contents (k5, &entry);
|
|
|
48b328 |
+ break;
|
|
|
48b328 |
+
|
|
|
48b328 |
+
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+ if (code == KRB5_KT_END)
|
|
|
48b328 |
+ code = 0;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ krb5_kt_end_seq_get (k5, keytab, &cursor);
|
|
|
48b328 |
+ return code;
|
|
|
48b328 |
+}
|
|
|
48b328 |
+
|
|
|
48b328 |
+krb5_error_code
|
|
|
48b328 |
+_adcli_krb5_keytab_copy_entries (krb5_context k5,
|
|
|
48b328 |
+ krb5_keytab keytab,
|
|
|
48b328 |
+ krb5_principal principal,
|
|
|
48b328 |
+ krb5_kvno kvno,
|
|
|
48b328 |
+ krb5_enctype *enctypes)
|
|
|
48b328 |
+{
|
|
|
48b328 |
+ krb5_keytab_entry entry;
|
|
|
48b328 |
+ krb5_error_code code;
|
|
|
48b328 |
+ int i;
|
|
|
48b328 |
+ match_enctype_kvno closure;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ for (i = 0; enctypes[i] != 0; i++) {
|
|
|
48b328 |
+
|
|
|
48b328 |
+ closure.kvno = kvno;
|
|
|
48b328 |
+ closure.enctype = enctypes[i];
|
|
|
48b328 |
+ closure.matched = 0;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ memset (&entry, 0, sizeof (entry));
|
|
|
48b328 |
+
|
|
|
48b328 |
+ code = _adcli_krb5_get_keyblock (k5, keytab, &entry.key,
|
|
|
48b328 |
+ match_enctype_and_kvno, &closure);
|
|
|
48b328 |
+ if (code != 0) {
|
|
|
48b328 |
+ return code;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+
|
|
|
48b328 |
+ entry.principal = principal;
|
|
|
48b328 |
+ entry.vno = kvno;
|
|
|
48b328 |
+
|
|
|
48b328 |
+ code = krb5_kt_add_entry (k5, keytab, &entry);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ entry.principal = NULL;
|
|
|
48b328 |
+ krb5_free_keytab_entry_contents (k5, &entry);
|
|
|
48b328 |
+
|
|
|
48b328 |
+ if (code != 0)
|
|
|
48b328 |
+ return code;
|
|
|
48b328 |
+ }
|
|
|
48b328 |
+
|
|
|
48b328 |
+ return 0;
|
|
|
48b328 |
+}
|
|
|
48b328 |
|
|
|
48b328 |
krb5_error_code
|
|
|
48b328 |
_adcli_krb5_keytab_add_entries (krb5_context k5,
|
|
|
48b328 |
diff --git a/library/adprivate.h b/library/adprivate.h
|
|
|
48b328 |
index 83a88f6..7485249 100644
|
|
|
48b328 |
--- a/library/adprivate.h
|
|
|
48b328 |
+++ b/library/adprivate.h
|
|
|
48b328 |
@@ -282,6 +282,12 @@ krb5_enctype * _adcli_krb5_parse_enctypes (const char *value);
|
|
|
48b328 |
|
|
|
48b328 |
char * _adcli_krb5_format_enctypes (krb5_enctype *enctypes);
|
|
|
48b328 |
|
|
|
48b328 |
+krb5_error_code _adcli_krb5_keytab_copy_entries (krb5_context k5,
|
|
|
48b328 |
+ krb5_keytab keytab,
|
|
|
48b328 |
+ krb5_principal principal,
|
|
|
48b328 |
+ krb5_kvno kvno,
|
|
|
48b328 |
+ krb5_enctype *enctypes);
|
|
|
48b328 |
+
|
|
|
48b328 |
struct _adcli_attrs {
|
|
|
48b328 |
LDAPMod **mods;
|
|
|
48b328 |
int len;
|
|
|
48b328 |
--
|
|
|
48b328 |
2.14.4
|
|
|
48b328 |
|