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