|
|
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 |
|