webhooks.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import hashlib
  2. import hmac
  3. import logging
  4. import requests
  5. from django.conf import settings
  6. from django_rq import job
  7. from jinja2.exceptions import TemplateError
  8. from .constants import WEBHOOK_EVENT_TYPES
  9. logger = logging.getLogger('netbox.webhooks')
  10. def generate_signature(request_body, secret):
  11. """
  12. Return a cryptographic signature that can be used to verify the authenticity of webhook data.
  13. """
  14. hmac_prep = hmac.new(
  15. key=secret.encode('utf8'),
  16. msg=request_body,
  17. digestmod=hashlib.sha512
  18. )
  19. return hmac_prep.hexdigest()
  20. @job('default')
  21. def send_webhook(event_rule, model_name, event_type, data, timestamp, username, request_id=None, snapshots=None):
  22. """
  23. Make a POST request to the defined Webhook
  24. """
  25. webhook = event_rule.action_object
  26. # Prepare context data for headers & body templates
  27. context = {
  28. 'event': WEBHOOK_EVENT_TYPES.get(event_type, event_type),
  29. 'timestamp': timestamp,
  30. 'model': model_name,
  31. 'username': username,
  32. 'request_id': request_id,
  33. 'data': data,
  34. }
  35. if snapshots:
  36. context.update({
  37. 'snapshots': snapshots
  38. })
  39. # Build the headers for the HTTP request
  40. headers = {
  41. 'Content-Type': webhook.http_content_type,
  42. }
  43. try:
  44. headers.update(webhook.render_headers(context))
  45. except (TemplateError, ValueError) as e:
  46. logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
  47. raise e
  48. # Render the request body
  49. try:
  50. body = webhook.render_body(context)
  51. except TemplateError as e:
  52. logger.error(f"Error rendering request body for webhook {webhook}: {e}")
  53. raise e
  54. # Prepare the HTTP request
  55. params = {
  56. 'method': webhook.http_method,
  57. 'url': webhook.render_payload_url(context),
  58. 'headers': headers,
  59. 'data': body.encode('utf8'),
  60. }
  61. logger.info(
  62. f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
  63. )
  64. logger.debug(params)
  65. try:
  66. prepared_request = requests.Request(**params).prepare()
  67. except requests.exceptions.RequestException as e:
  68. logger.error(f"Error forming HTTP request: {e}")
  69. raise e
  70. # If a secret key is defined, sign the request with a hash of the key and its content
  71. if webhook.secret != '':
  72. prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
  73. # Send the request
  74. with requests.Session() as session:
  75. session.verify = webhook.ssl_verification
  76. if webhook.ca_file_path:
  77. session.verify = webhook.ca_file_path
  78. response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
  79. if 200 <= response.status_code <= 299:
  80. logger.info(f"Request succeeded; response status {response.status_code}")
  81. return f"Status {response.status_code} returned, webhook successfully processed."
  82. else:
  83. logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
  84. raise requests.exceptions.RequestException(
  85. f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
  86. )