|
|
27390d |
From c0e4f8f27f0becd93c7abd9f20224232d5f1a5cf Mon Sep 17 00:00:00 2001
|
|
|
27390d |
From: Martin Milata <mmilata@redhat.com>
|
|
|
27390d |
Date: Thu, 16 Jan 2014 20:02:05 +0100
|
|
|
27390d |
Subject: [LIBREPORT PATCH 12/14] ureport: add support for client-side
|
|
|
27390d |
authentication
|
|
|
27390d |
|
|
|
27390d |
Please note that the libreport_curl api is changed and since we're not
|
|
|
27390d |
bumping sonames ABRT has to explicitly depend on this version in spec.
|
|
|
27390d |
|
|
|
27390d |
Related to rhbz#1053042.
|
|
|
27390d |
|
|
|
27390d |
Signed-off-by: Martin Milata <mmilata@redhat.com>
|
|
|
27390d |
---
|
|
|
27390d |
doc/reporter-ureport.txt | 18 ++++++++++
|
|
|
27390d |
src/include/libreport_curl.h | 2 ++
|
|
|
27390d |
src/lib/curl.c | 7 ++++
|
|
|
27390d |
src/lib/json.c | 48 ++++++++++++-------------
|
|
|
27390d |
src/lib/ureport.h | 7 ++--
|
|
|
27390d |
src/plugins/ureport.c | 85 ++++++++++++++++++++++++++++++++++++++++++--
|
|
|
27390d |
src/plugins/ureport.conf | 10 ++++++
|
|
|
27390d |
7 files changed, 149 insertions(+), 28 deletions(-)
|
|
|
27390d |
|
|
|
27390d |
diff --git a/doc/reporter-ureport.txt b/doc/reporter-ureport.txt
|
|
|
27390d |
index b739b6d..54823ae 100644
|
|
|
27390d |
--- a/doc/reporter-ureport.txt
|
|
|
27390d |
+++ b/doc/reporter-ureport.txt
|
|
|
27390d |
@@ -27,6 +27,20 @@ Configuration file lines should have 'PARAM = VALUE' format. The parameters are:
|
|
|
27390d |
'SSLVerify'::
|
|
|
27390d |
Use no/false/off/0 to disable verification of server's SSL certificate. (default: yes)
|
|
|
27390d |
|
|
|
27390d |
+'SSLClientAuth'::
|
|
|
27390d |
+ If this option is set, client-side SSL certificate is used to authenticate
|
|
|
27390d |
+ to the server so that it knows which machine it came from. Possible values
|
|
|
27390d |
+ are:
|
|
|
27390d |
+
|
|
|
27390d |
+ 'rhsm';;
|
|
|
27390d |
+ Uses the system certificate that is used for Red Hat subscription management.
|
|
|
27390d |
+
|
|
|
27390d |
+ 'puppet';;
|
|
|
27390d |
+ Uses the certificate that is used by the Puppet configuration management tool.
|
|
|
27390d |
+
|
|
|
27390d |
+ '<cert_path>:<key_path>';;
|
|
|
27390d |
+ Manually supply paths to certificate and the corresponding key in PEM format.
|
|
|
27390d |
+
|
|
|
27390d |
'ContactEmail'::
|
|
|
27390d |
Email address attached to a bthash on the server.
|
|
|
27390d |
|
|
|
27390d |
@@ -61,6 +75,10 @@ OPTIONS
|
|
|
27390d |
-k, --insecure::
|
|
|
27390d |
Allow insecure connection to ureport server
|
|
|
27390d |
|
|
|
27390d |
+-t, --auth SOURCE::
|
|
|
27390d |
+ Enables client authentication. See 'SSLClientAuth' configuration file
|
|
|
27390d |
+ option for list of possible values.
|
|
|
27390d |
+
|
|
|
27390d |
-v::
|
|
|
27390d |
Be more verbose. Can be given multiple times.
|
|
|
27390d |
|
|
|
27390d |
diff --git a/src/include/libreport_curl.h b/src/include/libreport_curl.h
|
|
|
27390d |
index 4cd855f..7d6fa02 100644
|
|
|
27390d |
--- a/src/include/libreport_curl.h
|
|
|
27390d |
+++ b/src/include/libreport_curl.h
|
|
|
27390d |
@@ -35,6 +35,8 @@ typedef struct post_state {
|
|
|
27390d |
int flags;
|
|
|
27390d |
const char *username;
|
|
|
27390d |
const char *password;
|
|
|
27390d |
+ const char *client_cert_path;
|
|
|
27390d |
+ const char *client_key_path;
|
|
|
27390d |
/* Results of POST transaction: */
|
|
|
27390d |
int http_resp_code;
|
|
|
27390d |
/* cast from CURLcode enum.
|
|
|
27390d |
diff --git a/src/lib/curl.c b/src/lib/curl.c
|
|
|
27390d |
index 6722b4a..662a2cf 100644
|
|
|
27390d |
--- a/src/lib/curl.c
|
|
|
27390d |
+++ b/src/lib/curl.c
|
|
|
27390d |
@@ -532,6 +532,13 @@ post(post_state_t *state,
|
|
|
27390d |
xcurl_easy_setopt_long(handle, CURLOPT_SSL_VERIFYPEER, 0);
|
|
|
27390d |
xcurl_easy_setopt_long(handle, CURLOPT_SSL_VERIFYHOST, 0);
|
|
|
27390d |
}
|
|
|
27390d |
+ if (state->client_cert_path && state->client_key_path)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ xcurl_easy_setopt_ptr(handle, CURLOPT_SSLCERTTYPE, "PEM");
|
|
|
27390d |
+ xcurl_easy_setopt_ptr(handle, CURLOPT_SSLKEYTYPE, "PEM");
|
|
|
27390d |
+ xcurl_easy_setopt_ptr(handle, CURLOPT_SSLCERT, state->client_cert_path);
|
|
|
27390d |
+ xcurl_easy_setopt_ptr(handle, CURLOPT_SSLKEY, state->client_key_path);
|
|
|
27390d |
+ }
|
|
|
27390d |
|
|
|
27390d |
// This is the place where everything happens.
|
|
|
27390d |
// Here errors are not limited to "out of memory", can't just die.
|
|
|
27390d |
diff --git a/src/lib/json.c b/src/lib/json.c
|
|
|
27390d |
index eb8e5ed..66db537 100644
|
|
|
27390d |
--- a/src/lib/json.c
|
|
|
27390d |
+++ b/src/lib/json.c
|
|
|
27390d |
@@ -68,7 +68,7 @@ char *new_json_attachment(const char *bthash, const char *type, const char *data
|
|
|
27390d |
return result;
|
|
|
27390d |
}
|
|
|
27390d |
|
|
|
27390d |
-struct post_state *post_ureport(const char *json_ureport, struct ureport_server_config *config)
|
|
|
27390d |
+struct post_state *post_ureport(const char *json, struct ureport_server_config *config)
|
|
|
27390d |
{
|
|
|
27390d |
int flags = POST_WANT_BODY | POST_WANT_ERROR_MSG;
|
|
|
27390d |
|
|
|
27390d |
@@ -77,6 +77,12 @@ struct post_state *post_ureport(const char *json_ureport, struct ureport_server_
|
|
|
27390d |
|
|
|
27390d |
struct post_state *post_state = new_post_state(flags);
|
|
|
27390d |
|
|
|
27390d |
+ if (config->ur_client_cert && config->ur_client_key)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ post_state->client_cert_path = config->ur_client_cert;
|
|
|
27390d |
+ post_state->client_key_path = config->ur_client_key;
|
|
|
27390d |
+ }
|
|
|
27390d |
+
|
|
|
27390d |
static const char *headers[] = {
|
|
|
27390d |
"Accept: application/json",
|
|
|
27390d |
"Connection: close",
|
|
|
27390d |
@@ -84,30 +90,24 @@ struct post_state *post_ureport(const char *json_ureport, struct ureport_server_
|
|
|
27390d |
};
|
|
|
27390d |
|
|
|
27390d |
post_string_as_form_data(post_state, config->ur_url, "application/json",
|
|
|
27390d |
- headers, json_ureport);
|
|
|
27390d |
+ headers, json);
|
|
|
27390d |
|
|
|
27390d |
- return post_state;
|
|
|
27390d |
-}
|
|
|
27390d |
+ /* Client authentication failed. Try again without client auth.
|
|
|
27390d |
+ * CURLE_SSL_CONNECT_ERROR - cert not found/server doesnt trust the CA
|
|
|
27390d |
+ * CURLE_SSL_CERTPROBLEM - malformed certificate/no permission
|
|
|
27390d |
+ */
|
|
|
27390d |
+ if ((post_state->curl_result == CURLE_SSL_CONNECT_ERROR
|
|
|
27390d |
+ || post_state->curl_result == CURLE_SSL_CERTPROBLEM)
|
|
|
27390d |
+ && config->ur_client_cert && config->ur_client_key)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ warn_msg("Authentication failed. Retrying unauthenticated.");
|
|
|
27390d |
+ free_post_state(post_state);
|
|
|
27390d |
+ post_state = new_post_state(flags);
|
|
|
27390d |
|
|
|
27390d |
-static
|
|
|
27390d |
-struct post_state *ureport_attach(const char *json_attachment,
|
|
|
27390d |
- struct ureport_server_config *config)
|
|
|
27390d |
-{
|
|
|
27390d |
- int flags = POST_WANT_BODY | POST_WANT_ERROR_MSG;
|
|
|
27390d |
+ post_string_as_form_data(post_state, config->ur_url, "application/json",
|
|
|
27390d |
+ headers, json);
|
|
|
27390d |
|
|
|
27390d |
- if (config->ur_ssl_verify)
|
|
|
27390d |
- flags |= POST_WANT_SSL_VERIFY;
|
|
|
27390d |
-
|
|
|
27390d |
- struct post_state *post_state = new_post_state(flags);
|
|
|
27390d |
-
|
|
|
27390d |
- static const char *headers[] = {
|
|
|
27390d |
- "Accept: application/json",
|
|
|
27390d |
- "Connection: close",
|
|
|
27390d |
- NULL,
|
|
|
27390d |
- };
|
|
|
27390d |
-
|
|
|
27390d |
- post_string_as_form_data(post_state, config->ur_url, "application/json",
|
|
|
27390d |
- headers, json_attachment);
|
|
|
27390d |
+ }
|
|
|
27390d |
|
|
|
27390d |
return post_state;
|
|
|
27390d |
}
|
|
|
27390d |
@@ -117,7 +117,7 @@ struct post_state *ureport_attach_rhbz(const char *bthash, int rhbz_bug_id,
|
|
|
27390d |
{
|
|
|
27390d |
char *str_bug_id = xasprintf("%d", rhbz_bug_id);
|
|
|
27390d |
char *json_attachment = new_json_attachment(bthash, "RHBZ", str_bug_id);
|
|
|
27390d |
- struct post_state *post_state = ureport_attach(json_attachment, config);
|
|
|
27390d |
+ struct post_state *post_state = post_ureport(json_attachment, config);
|
|
|
27390d |
free(str_bug_id);
|
|
|
27390d |
free(json_attachment);
|
|
|
27390d |
|
|
|
27390d |
@@ -128,7 +128,7 @@ struct post_state *ureport_attach_email(const char *bthash, const char *email,
|
|
|
27390d |
struct ureport_server_config *config)
|
|
|
27390d |
{
|
|
|
27390d |
char *json_attachment = new_json_attachment(bthash, "email", email);
|
|
|
27390d |
- struct post_state *post_state = ureport_attach(json_attachment, config);
|
|
|
27390d |
+ struct post_state *post_state = post_ureport(json_attachment, config);
|
|
|
27390d |
free(json_attachment);
|
|
|
27390d |
|
|
|
27390d |
return post_state;
|
|
|
27390d |
diff --git a/src/lib/ureport.h b/src/lib/ureport.h
|
|
|
27390d |
index 4cc4e10..16f40f1 100644
|
|
|
27390d |
--- a/src/lib/ureport.h
|
|
|
27390d |
+++ b/src/lib/ureport.h
|
|
|
27390d |
@@ -30,8 +30,11 @@ extern "C" {
|
|
|
27390d |
*/
|
|
|
27390d |
struct ureport_server_config
|
|
|
27390d |
{
|
|
|
27390d |
- const char *ur_url; ///< Web service URL
|
|
|
27390d |
- bool ur_ssl_verify; ///< Verify HOST and PEER certificates
|
|
|
27390d |
+ const char *ur_url; ///< Web service URL
|
|
|
27390d |
+ bool ur_ssl_verify; ///< Verify HOST and PEER certificates
|
|
|
27390d |
+ char *ur_client_cert; ///< Path to certificate used for client
|
|
|
27390d |
+ ///< authentication (or NULL)
|
|
|
27390d |
+ char *ur_client_key; ///< Private key for the certificate
|
|
|
27390d |
};
|
|
|
27390d |
|
|
|
27390d |
struct abrt_post_state;
|
|
|
27390d |
diff --git a/src/plugins/ureport.c b/src/plugins/ureport.c
|
|
|
27390d |
index 0168744..b57eada 100644
|
|
|
27390d |
--- a/src/plugins/ureport.c
|
|
|
27390d |
+++ b/src/plugins/ureport.c
|
|
|
27390d |
@@ -28,10 +28,73 @@
|
|
|
27390d |
#define ATTACH_URL_SFX "reports/attach/"
|
|
|
27390d |
#define BTHASH_URL_SFX "reports/bthash/"
|
|
|
27390d |
|
|
|
27390d |
+#define RHSM_CERT_PATH "/etc/pki/consumer/cert.pem"
|
|
|
27390d |
+#define RHSM_KEY_PATH "/etc/pki/consumer/key.pem"
|
|
|
27390d |
+
|
|
|
27390d |
#define VALUE_FROM_CONF(opt, var, tr) do { const char *value = getenv("uReport_"opt); \
|
|
|
27390d |
if (!value) { value = get_map_string_item_or_NULL(settings, opt); } if (value) { var = tr(value); } \
|
|
|
27390d |
} while(0)
|
|
|
27390d |
|
|
|
27390d |
+static char *puppet_config_print(const char *key)
|
|
|
27390d |
+{
|
|
|
27390d |
+ char *command = xasprintf("puppet config print %s", key);
|
|
|
27390d |
+ char *result = run_in_shell_and_save_output(0, command, NULL, NULL);
|
|
|
27390d |
+ free(command);
|
|
|
27390d |
+
|
|
|
27390d |
+ /* run_in_shell_and_save_output always returns non-NULL */
|
|
|
27390d |
+ if (result[0] != '/')
|
|
|
27390d |
+ goto error;
|
|
|
27390d |
+
|
|
|
27390d |
+ char *newline = strchrnul(result, '\n');
|
|
|
27390d |
+ if (!newline)
|
|
|
27390d |
+ goto error;
|
|
|
27390d |
+
|
|
|
27390d |
+ *newline = '\0';
|
|
|
27390d |
+ return result;
|
|
|
27390d |
+error:
|
|
|
27390d |
+ free(result);
|
|
|
27390d |
+ error_msg_and_die("Unable to determine puppet %s path (puppet not installed?)", key);
|
|
|
27390d |
+}
|
|
|
27390d |
+
|
|
|
27390d |
+static void parse_client_auth_paths(struct ureport_server_config *config, const char *client_auth)
|
|
|
27390d |
+{
|
|
|
27390d |
+ if (client_auth == NULL)
|
|
|
27390d |
+ return;
|
|
|
27390d |
+
|
|
|
27390d |
+ if (strcmp(client_auth, "") == 0)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ config->ur_client_cert = NULL;
|
|
|
27390d |
+ config->ur_client_key = NULL;
|
|
|
27390d |
+ log_notice("Not using client authentication");
|
|
|
27390d |
+ }
|
|
|
27390d |
+ else if (strcmp(client_auth, "rhsm") == 0)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ config->ur_client_cert = xstrdup(RHSM_CERT_PATH);
|
|
|
27390d |
+ config->ur_client_key = xstrdup(RHSM_KEY_PATH);
|
|
|
27390d |
+ }
|
|
|
27390d |
+ else if (strcmp(client_auth, "puppet") == 0)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ config->ur_client_cert = puppet_config_print("hostcert");
|
|
|
27390d |
+ config->ur_client_key = puppet_config_print("hostprivkey");
|
|
|
27390d |
+ }
|
|
|
27390d |
+ else
|
|
|
27390d |
+ {
|
|
|
27390d |
+ char *scratch = xstrdup(client_auth);
|
|
|
27390d |
+ config->ur_client_cert = xstrdup(strtok(scratch, ":"));
|
|
|
27390d |
+ config->ur_client_key = xstrdup(strtok(NULL, ":"));
|
|
|
27390d |
+ free(scratch);
|
|
|
27390d |
+
|
|
|
27390d |
+ if (config->ur_client_cert == NULL || config->ur_client_key == NULL)
|
|
|
27390d |
+ error_msg_and_die("Invalid client authentication specification");
|
|
|
27390d |
+ }
|
|
|
27390d |
+
|
|
|
27390d |
+ if (config->ur_client_cert && config->ur_client_key)
|
|
|
27390d |
+ {
|
|
|
27390d |
+ log_notice("Using client certificate: %s", config->ur_client_cert);
|
|
|
27390d |
+ log_notice("Using client private key: %s", config->ur_client_key);
|
|
|
27390d |
+ }
|
|
|
27390d |
+}
|
|
|
27390d |
+
|
|
|
27390d |
/*
|
|
|
27390d |
* Loads uReport configuration from various sources.
|
|
|
27390d |
*
|
|
|
27390d |
@@ -44,6 +107,10 @@ static void load_ureport_server_config(struct ureport_server_config *config, map
|
|
|
27390d |
{
|
|
|
27390d |
VALUE_FROM_CONF("URL", config->ur_url, (const char *));
|
|
|
27390d |
VALUE_FROM_CONF("SSLVerify", config->ur_ssl_verify, string_to_bool);
|
|
|
27390d |
+
|
|
|
27390d |
+ const char *client_auth = NULL;
|
|
|
27390d |
+ VALUE_FROM_CONF("SSLClientAuth", client_auth, (const char *));
|
|
|
27390d |
+ parse_client_auth_paths(config, client_auth);
|
|
|
27390d |
}
|
|
|
27390d |
|
|
|
27390d |
struct ureport_server_response {
|
|
|
27390d |
@@ -243,7 +310,12 @@ static struct ureport_server_response *ureport_server_parse_json(json_object *js
|
|
|
27390d |
|
|
|
27390d |
static struct ureport_server_response *get_server_response(post_state_t *post_state, struct ureport_server_config *config)
|
|
|
27390d |
{
|
|
|
27390d |
- if (post_state->errmsg[0] != '\0')
|
|
|
27390d |
+ /* Previously, the condition here was (post_state->errmsg[0] != '\0')
|
|
|
27390d |
+ * however when the server asks for optional client authentication and we do not have the certificates,
|
|
|
27390d |
+ * then post_state->errmsg contains "NSS: client certificate not found (nickname not specified)" even though
|
|
|
27390d |
+ * the request succeeded.
|
|
|
27390d |
+ */
|
|
|
27390d |
+ if (post_state->curl_result != CURLE_OK)
|
|
|
27390d |
{
|
|
|
27390d |
error_msg(_("Failed to upload uReport to the server '%s' with curl: %s"), config->ur_url, post_state->errmsg);
|
|
|
27390d |
return NULL;
|
|
|
27390d |
@@ -349,6 +421,8 @@ int main(int argc, char **argv)
|
|
|
27390d |
struct ureport_server_config config = {
|
|
|
27390d |
.ur_url = NULL,
|
|
|
27390d |
.ur_ssl_verify = true,
|
|
|
27390d |
+ .ur_client_cert = NULL,
|
|
|
27390d |
+ .ur_client_key = NULL,
|
|
|
27390d |
};
|
|
|
27390d |
|
|
|
27390d |
enum {
|
|
|
27390d |
@@ -356,12 +430,14 @@ int main(int argc, char **argv)
|
|
|
27390d |
OPT_d = 1 << 1,
|
|
|
27390d |
OPT_u = 1 << 2,
|
|
|
27390d |
OPT_k = 1 << 3,
|
|
|
27390d |
+ OPT_t = 1 << 4,
|
|
|
27390d |
};
|
|
|
27390d |
|
|
|
27390d |
int ret = 1; /* "failure" (for now) */
|
|
|
27390d |
bool insecure = !config.ur_ssl_verify;
|
|
|
27390d |
const char *conf_file = CONF_FILE_PATH;
|
|
|
27390d |
const char *arg_server_url = NULL;
|
|
|
27390d |
+ const char *client_auth = NULL;
|
|
|
27390d |
const char *dump_dir_path = ".";
|
|
|
27390d |
const char *ureport_hash = NULL;
|
|
|
27390d |
bool ureport_hash_from_rt = false;
|
|
|
27390d |
@@ -376,6 +452,7 @@ int main(int argc, char **argv)
|
|
|
27390d |
OPT_STRING('u', "url", &arg_server_url, "URL", _("Specify server URL")),
|
|
|
27390d |
OPT_BOOL('k', "insecure", &insecure,
|
|
|
27390d |
_("Allow insecure connection to ureport server")),
|
|
|
27390d |
+ OPT_STRING('t', "auth", &client_auth, "SOURCE", _("Use client authentication")),
|
|
|
27390d |
OPT_STRING('c', NULL, &conf_file, "FILE", _("Configuration file")),
|
|
|
27390d |
OPT_STRING('a', "attach", &ureport_hash, "BTHASH",
|
|
|
27390d |
_("bthash of uReport to attach (conflicts with -A)")),
|
|
|
27390d |
@@ -393,7 +470,7 @@ int main(int argc, char **argv)
|
|
|
27390d |
};
|
|
|
27390d |
|
|
|
27390d |
const char *program_usage_string = _(
|
|
|
27390d |
- "& [-v] [-c FILE] [-u URL] [-k] [-A -a bthash -B -b bug-id -E -e email] [-d DIR]\n"
|
|
|
27390d |
+ "& [-v] [-c FILE] [-u URL] [-k] [-t SOURCE] [-A -a bthash -B -b bug-id -E -e email] [-d DIR]\n"
|
|
|
27390d |
"\n"
|
|
|
27390d |
"Upload micro report or add an attachment to a micro report\n"
|
|
|
27390d |
"\n"
|
|
|
27390d |
@@ -411,6 +488,8 @@ int main(int argc, char **argv)
|
|
|
27390d |
config.ur_url = arg_server_url;
|
|
|
27390d |
if (opts & OPT_k)
|
|
|
27390d |
config.ur_ssl_verify = !insecure;
|
|
|
27390d |
+ if (opts & OPT_t)
|
|
|
27390d |
+ parse_client_auth_paths(&config, client_auth);
|
|
|
27390d |
|
|
|
27390d |
if (!config.ur_url)
|
|
|
27390d |
error_msg_and_die("You need to specify server URL");
|
|
|
27390d |
@@ -580,6 +659,8 @@ format_err:
|
|
|
27390d |
|
|
|
27390d |
finalize:
|
|
|
27390d |
free_map_string(settings);
|
|
|
27390d |
+ free(config.ur_client_cert);
|
|
|
27390d |
+ free(config.ur_client_key);
|
|
|
27390d |
|
|
|
27390d |
return ret;
|
|
|
27390d |
}
|
|
|
27390d |
diff --git a/src/plugins/ureport.conf b/src/plugins/ureport.conf
|
|
|
27390d |
index 1f3b33a..13b6386 100644
|
|
|
27390d |
--- a/src/plugins/ureport.conf
|
|
|
27390d |
+++ b/src/plugins/ureport.conf
|
|
|
27390d |
@@ -6,3 +6,13 @@ URL = http://bug-report.itos.redhat.com
|
|
|
27390d |
|
|
|
27390d |
# Contact email attached to an uploaded uReport if required
|
|
|
27390d |
# ContactEmail = foo@example.com
|
|
|
27390d |
+
|
|
|
27390d |
+# Client-side authentication
|
|
|
27390d |
+# None (default):
|
|
|
27390d |
+# SSLClientAuth =
|
|
|
27390d |
+# Using RH subscription management certificate:
|
|
|
27390d |
+# SSLClientAuth = rhsm
|
|
|
27390d |
+# Using Puppet certificate:
|
|
|
27390d |
+# SSLClientAuth = puppet
|
|
|
27390d |
+# Using custom certificate:
|
|
|
27390d |
+# SSLClientAuth = /path/to/cert.pem:/path/to/key.pem
|
|
|
27390d |
--
|
|
|
27390d |
1.8.3.1
|
|
|
27390d |
|