| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import hashlib
- import hmac
- import logging
- import requests
- from django_rq import job
- from jinja2.exceptions import TemplateError
- from netbox.registry import registry
- from utilities.proxy import resolve_proxies
- from .constants import WEBHOOK_EVENT_TYPES
- __all__ = (
- 'generate_signature',
- 'register_webhook_callback',
- 'send_webhook',
- )
- logger = logging.getLogger('netbox.webhooks')
- def register_webhook_callback(func):
- """
- Register a function as a webhook callback.
- """
- registry['webhook_callbacks'].append(func)
- logger.debug(f'Registered webhook callback {func.__module__}.{func.__name__}')
- return func
- def generate_signature(request_body, secret):
- """
- Return a cryptographic signature that can be used to verify the authenticity of webhook data.
- """
- hmac_prep = hmac.new(
- key=secret.encode('utf8'),
- msg=request_body,
- digestmod=hashlib.sha512
- )
- return hmac_prep.hexdigest()
- @job('default')
- def send_webhook(event_rule, object_type, event_type, data, timestamp, username, request=None, snapshots=None):
- """
- Make a POST request to the defined Webhook
- """
- webhook = event_rule.action_object
- # Prepare context data for headers & body templates
- context = {
- 'event': WEBHOOK_EVENT_TYPES.get(event_type, event_type),
- 'timestamp': timestamp,
- 'object_type': '.'.join(object_type.natural_key()),
- 'model': object_type.model,
- 'username': username,
- 'request_id': request.id if request else None,
- 'data': data,
- }
- if snapshots:
- context.update({
- 'snapshots': snapshots
- })
- # Add any additional context from plugins
- callback_data = {}
- for callback in registry['webhook_callbacks']:
- try:
- if ret := callback(object_type, event_type, data, request):
- callback_data.update(**ret)
- except Exception as e:
- logger.warning(f"Caught exception when processing callback {callback}: {e}")
- pass
- if callback_data:
- context['context'] = callback_data
- # Build the headers for the HTTP request
- headers = {
- 'Content-Type': webhook.http_content_type,
- }
- try:
- headers.update(webhook.render_headers(context))
- except (TemplateError, ValueError) as e:
- logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
- raise e
- # Render the request body
- try:
- body = webhook.render_body(context)
- except TemplateError as e:
- logger.error(f"Error rendering request body for webhook {webhook}: {e}")
- raise e
- # Prepare the HTTP request
- url = webhook.render_payload_url(context)
- params = {
- 'method': webhook.http_method,
- 'url': url,
- 'headers': headers,
- 'data': body.encode('utf8'),
- }
- logger.info(
- f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
- )
- logger.debug(params)
- try:
- prepared_request = requests.Request(**params).prepare()
- except requests.exceptions.RequestException as e:
- logger.error(f"Error forming HTTP request: {e}")
- raise e
- # If a secret key is defined, sign the request with a hash of the key and its content
- if webhook.secret != '':
- prepared_request.headers['X-Hook-Signature'] = generate_signature(prepared_request.body, webhook.secret)
- # Send the request
- with requests.Session() as session:
- session.verify = webhook.ssl_verification
- if webhook.ca_file_path:
- session.verify = webhook.ca_file_path
- proxies = resolve_proxies(url=url, context={'client': webhook})
- response = session.send(prepared_request, proxies=proxies)
- if 200 <= response.status_code <= 299:
- logger.info(f"Request succeeded; response status {response.status_code}")
- return f"Status {response.status_code} returned, webhook successfully processed."
- else:
- logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
- raise requests.exceptions.RequestException(
- f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
- )
|