webhooks.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import hashlib
  2. import hmac
  3. import logging
  4. import requests
  5. from django_rq import job
  6. from jinja2.exceptions import TemplateError
  7. from netbox.registry import registry
  8. from utilities.proxy import resolve_proxies
  9. from .constants import WEBHOOK_EVENT_TYPES
  10. __all__ = (
  11. 'generate_signature',
  12. 'register_webhook_callback',
  13. 'send_webhook',
  14. )
  15. logger = logging.getLogger('netbox.webhooks')
  16. def register_webhook_callback(func):
  17. """
  18. Register a function as a webhook callback.
  19. """
  20. registry['webhook_callbacks'].append(func)
  21. logger.debug(f'Registered webhook callback {func.__module__}.{func.__name__}')
  22. return func
  23. def generate_signature(request_body, secret):
  24. """
  25. Return a cryptographic signature that can be used to verify the authenticity of webhook data.
  26. """
  27. hmac_prep = hmac.new(
  28. key=secret.encode('utf8'),
  29. msg=request_body,
  30. digestmod=hashlib.sha512
  31. )
  32. return hmac_prep.hexdigest()
  33. @job('default')
  34. def send_webhook(event_rule, object_type, event_type, data, timestamp, username, request=None, snapshots=None):
  35. """
  36. Make a POST request to the defined Webhook
  37. """
  38. webhook = event_rule.action_object
  39. # Prepare context data for headers & body templates
  40. context = {
  41. 'event': WEBHOOK_EVENT_TYPES.get(event_type, event_type),
  42. 'timestamp': timestamp,
  43. 'object_type': '.'.join(object_type.natural_key()),
  44. 'model': object_type.model,
  45. 'username': username,
  46. 'request_id': request.id if request else None,
  47. 'data': data,
  48. }
  49. if snapshots:
  50. context.update({
  51. 'snapshots': snapshots
  52. })
  53. # Add any additional context from plugins
  54. callback_data = {}
  55. for callback in registry['webhook_callbacks']:
  56. try:
  57. if ret := callback(object_type, event_type, data, request):
  58. callback_data.update(**ret)
  59. except Exception as e:
  60. logger.warning(f"Caught exception when processing callback {callback}: {e}")
  61. pass
  62. if callback_data:
  63. context['context'] = callback_data
  64. # Build the headers for the HTTP request
  65. headers = {
  66. 'Content-Type': webhook.http_content_type,
  67. }
  68. try:
  69. headers.update(webhook.render_headers(context))
  70. except (TemplateError, ValueError) as e:
  71. logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
  72. raise e
  73. # Render the request body
  74. try:
  75. body = webhook.render_body(context)
  76. except TemplateError as e:
  77. logger.error(f"Error rendering request body for webhook {webhook}: {e}")
  78. raise e
  79. # Prepare the HTTP request
  80. url = webhook.render_payload_url(context)
  81. params = {
  82. 'method': webhook.http_method,
  83. 'url': url,
  84. 'headers': headers,
  85. 'data': body.encode('utf8'),
  86. }
  87. logger.info(
  88. f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
  89. )
  90. logger.debug(params)
  91. try:
  92. prepared_request = requests.Request(**params).prepare()
  93. except requests.exceptions.RequestException as e:
  94. logger.error(f"Error forming HTTP request: {e}")
  95. raise e
  96. # If a secret key is defined, sign the request with a hash of the key and its content
  97. if webhook.secret != '':
  98. prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
  99. # Send the request
  100. with requests.Session() as session:
  101. session.verify = webhook.ssl_verification
  102. if webhook.ca_file_path:
  103. session.verify = webhook.ca_file_path
  104. proxies = resolve_proxies(url=url, context={'client': webhook})
  105. response = session.send(prepared_request, proxies=proxies)
  106. if 200 <= response.status_code <= 299:
  107. logger.info(f"Request succeeded; response status {response.status_code}")
  108. return f"Status {response.status_code} returned, webhook successfully processed."
  109. else:
  110. logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
  111. raise requests.exceptions.RequestException(
  112. f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
  113. )