a3470f
From cee93742430f0ecd3defb65e5ca62ef37f581703 Mon Sep 17 00:00:00 2001
a3470f
From: Aravinda VK <avishwan@redhat.com>
a3470f
Date: Tue, 17 Oct 2017 12:50:48 +0530
a3470f
Subject: [PATCH 116/128] eventsapi: HTTPS support for Webhooks
a3470f
a3470f
First it tries to call URL with verify=True without specifying the cert
a3470f
path, it succeeds if a webhook is HTTP or HTTPS with CA trusted
a3470f
certificates(for example https://github..).
a3470f
a3470f
If above call fails with SSL error then it tries to get the server
a3470f
certificate and calls URL again. If call fails with SSL error even after
a3470f
using the certificate, then verification will be disabled and logged in
a3470f
the log file.
a3470f
a3470f
All other errors will be catched and logged as usual.
a3470f
a3470f
>upstream mainline patch : https://review.gluster.org/18578
a3470f
a3470f
BUG: 1466122
a3470f
Change-Id: I86a3390ed48b75dffdc7848022af23a1e1d7f076
a3470f
Signed-off-by: Aravinda VK <avishwan@redhat.com>
a3470f
Reviewed-on: https://code.engineering.redhat.com/gerrit/126618
a3470f
Tested-by: RHGS Build Bot <nigelb@redhat.com>
a3470f
Reviewed-by: Atin Mukherjee <amukherj@redhat.com>
a3470f
---
a3470f
 events/src/eventsapiconf.py.in |  1 +
a3470f
 events/src/peer_eventsapi.py   | 48 +++++++++++++++++----
a3470f
 events/src/utils.py            | 94 ++++++++++++++++++++++++++++++++----------
a3470f
 3 files changed, 114 insertions(+), 29 deletions(-)
a3470f
a3470f
diff --git a/events/src/eventsapiconf.py.in b/events/src/eventsapiconf.py.in
a3470f
index 08a3602..687eaa3 100644
a3470f
--- a/events/src/eventsapiconf.py.in
a3470f
+++ b/events/src/eventsapiconf.py.in
a3470f
@@ -26,6 +26,7 @@ UUID_FILE = "@GLUSTERD_WORKDIR@/glusterd.info"
a3470f
 PID_FILE = "@localstatedir@/run/glustereventsd.pid"
a3470f
 AUTO_BOOL_ATTRIBUTES = ["force", "push-pem", "no-verify"]
a3470f
 AUTO_INT_ATTRIBUTES = ["ssh-port"]
a3470f
+CERTS_DIR = "@GLUSTERD_WORKDIR@/events"
a3470f
 
a3470f
 # Errors
a3470f
 ERROR_SAME_CONFIG = 2
a3470f
diff --git a/events/src/peer_eventsapi.py b/events/src/peer_eventsapi.py
a3470f
index 3a6a0eb..d72fdbe 100644
a3470f
--- a/events/src/peer_eventsapi.py
a3470f
+++ b/events/src/peer_eventsapi.py
a3470f
@@ -27,7 +27,7 @@ from gluster.cliutils import (Cmd, node_output_ok, node_output_notok,
a3470f
                               sync_file_to_peers, GlusterCmdException,
a3470f
                               output_error, execute_in_peers, runcli,
a3470f
                               set_common_args_func)
a3470f
-from events.utils import LockedOpen, get_jwt_token
a3470f
+from events.utils import LockedOpen, get_jwt_token, save_https_cert
a3470f
 
a3470f
 from events.eventsapiconf import (WEBHOOKS_FILE_TO_SYNC,
a3470f
                                   WEBHOOKS_FILE,
a3470f
@@ -47,7 +47,8 @@ from events.eventsapiconf import (WEBHOOKS_FILE_TO_SYNC,
a3470f
                                   ERROR_PARTIAL_SUCCESS,
a3470f
                                   ERROR_ALL_NODES_STATUS_NOT_OK,
a3470f
                                   ERROR_SAME_CONFIG,
a3470f
-                                  ERROR_WEBHOOK_SYNC_FAILED)
a3470f
+                                  ERROR_WEBHOOK_SYNC_FAILED,
a3470f
+                                  CERTS_DIR)
a3470f
 
a3470f
 
a3470f
 def handle_output_error(err, errcode=1, json_output=False):
a3470f
@@ -405,12 +406,43 @@ class NodeWebhookTestCmd(Cmd):
a3470f
         if hashval:
a3470f
             http_headers["Authorization"] = "Bearer " + hashval
a3470f
 
a3470f
-        try:
a3470f
-            resp = requests.post(args.url, headers=http_headers)
a3470f
-        except requests.ConnectionError as e:
a3470f
-            node_output_notok("{0}".format(e))
a3470f
-        except requests.exceptions.InvalidSchema as e:
a3470f
-            node_output_notok("{0}".format(e))
a3470f
+        urldata = requests.utils.urlparse(args.url)
a3470f
+        parts = urldata.netloc.split(":")
a3470f
+        domain = parts[0]
a3470f
+        # Default https port if not specified
a3470f
+        port = 443
a3470f
+        if len(parts) == 2:
a3470f
+            port = int(parts[1])
a3470f
+
a3470f
+        cert_path = os.path.join(CERTS_DIR, args.url.replace("/", "_").strip())
a3470f
+        verify = True
a3470f
+        while True:
a3470f
+            try:
a3470f
+                resp = requests.post(args.url, headers=http_headers,
a3470f
+                                     verify=verify)
a3470f
+                # Successful webhook push
a3470f
+                break
a3470f
+            except requests.exceptions.SSLError as e:
a3470f
+                # If verify is equal to cert path, but still failed with
a3470f
+                # SSLError, Looks like some issue with custom downloaded
a3470f
+                # certificate, Try with verify = false
a3470f
+                if verify == cert_path:
a3470f
+                    verify = False
a3470f
+                    continue
a3470f
+
a3470f
+                # If verify is instance of bool and True, then custom cert
a3470f
+                # is required, download the cert and retry
a3470f
+                try:
a3470f
+                    save_https_cert(domain, port, cert_path)
a3470f
+                    verify = cert_path
a3470f
+                except Exception:
a3470f
+                    verify = False
a3470f
+
a3470f
+                # Done with collecting cert, continue
a3470f
+                continue
a3470f
+            except Exception as e:
a3470f
+                node_output_notok("{0}".format(e))
a3470f
+                break
a3470f
 
a3470f
         if resp.status_code != 200:
a3470f
             node_output_notok("{0}".format(resp.status_code))
a3470f
diff --git a/events/src/utils.py b/events/src/utils.py
a3470f
index f24d64d..f405e44 100644
a3470f
--- a/events/src/utils.py
a3470f
+++ b/events/src/utils.py
a3470f
@@ -27,7 +27,8 @@ from eventsapiconf import (LOG_FILE,
a3470f
                            WEBHOOKS_FILE,
a3470f
                            DEFAULT_CONFIG_FILE,
a3470f
                            CUSTOM_CONFIG_FILE,
a3470f
-                           UUID_FILE)
a3470f
+                           UUID_FILE,
a3470f
+                           CERTS_DIR)
a3470f
 import eventtypes
a3470f
 
a3470f
 
a3470f
@@ -209,11 +210,33 @@ def get_jwt_token(secret, event_type, event_ts, jwt_expiry_time_seconds=60):
a3470f
     )
a3470f
 
a3470f
 
a3470f
+def save_https_cert(domain, port, cert_path):
a3470f
+    import ssl
a3470f
+
a3470f
+    # Cert file already available for this URL
a3470f
+    if os.path.exists(cert_path):
a3470f
+        return
a3470f
+
a3470f
+    cert_data = ssl.get_server_certificate((domain, port))
a3470f
+    with open(cert_path, "w") as f:
a3470f
+        f.write(cert_data)
a3470f
+
a3470f
+
a3470f
 def publish_to_webhook(url, token, secret, message_queue):
a3470f
     # Import requests here since not used in any other place
a3470f
     import requests
a3470f
 
a3470f
     http_headers = {"Content-Type": "application/json"}
a3470f
+    urldata = requests.utils.urlparse(url)
a3470f
+    parts = urldata.netloc.split(":")
a3470f
+    domain = parts[0]
a3470f
+    # Default https port if not specified
a3470f
+    port = 443
a3470f
+    if len(parts) == 2:
a3470f
+        port = int(parts[1])
a3470f
+
a3470f
+    cert_path = os.path.join(CERTS_DIR, url.replace("/", "_").strip())
a3470f
+
a3470f
     while True:
a3470f
         hashval = ""
a3470f
         event_type, event_ts, message_json = message_queue.get()
a3470f
@@ -226,26 +249,55 @@ def publish_to_webhook(url, token, secret, message_queue):
a3470f
         if hashval:
a3470f
             http_headers["Authorization"] = "Bearer " + hashval
a3470f
 
a3470f
-        try:
a3470f
-            resp = requests.post(url, headers=http_headers, data=message_json)
a3470f
-        except requests.ConnectionError as e:
a3470f
-            logger.warn("Event push failed to URL: {url}, "
a3470f
-                        "Event: {event}, "
a3470f
-                        "Status: {error}".format(
a3470f
-                            url=url,
a3470f
-                            event=message_json,
a3470f
-                            error=e))
a3470f
-            continue
a3470f
-        finally:
a3470f
-            message_queue.task_done()
a3470f
-
a3470f
-        if resp.status_code != 200:
a3470f
-            logger.warn("Event push failed to URL: {url}, "
a3470f
-                        "Event: {event}, "
a3470f
-                        "Status Code: {status_code}".format(
a3470f
-                            url=url,
a3470f
-                            event=message_json,
a3470f
-                            status_code=resp.status_code))
a3470f
+        verify = True
a3470f
+        while True:
a3470f
+            try:
a3470f
+                resp = requests.post(url, headers=http_headers,
a3470f
+                                     data=message_json,
a3470f
+                                     verify=verify)
a3470f
+                # Successful webhook push
a3470f
+                message_queue.task_done()
a3470f
+                if resp.status_code != 200:
a3470f
+                    logger.warn("Event push failed to URL: {url}, "
a3470f
+                                "Event: {event}, "
a3470f
+                                "Status Code: {status_code}".format(
a3470f
+                                    url=url,
a3470f
+                                    event=message_json,
a3470f
+                                    status_code=resp.status_code))
a3470f
+                break
a3470f
+            except requests.exceptions.SSLError as e:
a3470f
+                # If verify is equal to cert path, but still failed with
a3470f
+                # SSLError, Looks like some issue with custom downloaded
a3470f
+                # certificate, Try with verify = false
a3470f
+                if verify == cert_path:
a3470f
+                    logger.warn("Event push failed with certificate, "
a3470f
+                                "ignoring verification url={0} "
a3470f
+                                "Error={1}".format(url, e))
a3470f
+                    verify = False
a3470f
+                    continue
a3470f
+
a3470f
+                # If verify is instance of bool and True, then custom cert
a3470f
+                # is required, download the cert and retry
a3470f
+                try:
a3470f
+                    save_https_cert(domain, port, cert_path)
a3470f
+                    verify = cert_path
a3470f
+                except Exception as ex:
a3470f
+                    verify = False
a3470f
+                    logger.warn("Unable to get Server certificate, "
a3470f
+                                "ignoring verification url={0} "
a3470f
+                                "Error={1}".format(url, ex))
a3470f
+
a3470f
+                # Done with collecting cert, continue
a3470f
+                continue
a3470f
+            except Exception as e:
a3470f
+                logger.warn("Event push failed to URL: {url}, "
a3470f
+                            "Event: {event}, "
a3470f
+                            "Status: {error}".format(
a3470f
+                                url=url,
a3470f
+                                event=message_json,
a3470f
+                                error=e))
a3470f
+                message_queue.task_done()
a3470f
+                break
a3470f
 
a3470f
 
a3470f
 def plugin_webhook(message):
a3470f
-- 
a3470f
1.8.3.1
a3470f