Repository: netbox-community/netbox

Issue:
Add a built-in webhook listener to aid in troubleshooting
### Environment
* Python version: 3.6.9
* NetBox version: 2.7.11

### Proposed Functionality
Add a new management command that will initialize a simple HTTP daemon and print to terminal all received HTTP requests. For example:

```
$ python manage.py webhook_receiver 0.0.0.0:9000
Listening on 0.0.0.0:9000...
[1] Received request from 192.0.2.48
  Type: POST
  Headers: <...>
  Body: <...>
```

### Use Case
It can be difficult to troubleshoot webhooks, especially when writing custom header/body templates. Introducing a local service to dump received requests allows a user to perform local troubleshooting by pointing a webhook to the local host on the specified port.

### Database Changes
None

### External Dependencies
None


Discussion:

Resolving patch:
diff --git a/netbox/extras/management/commands/webhook_receiver.py b/netbox/extras/management/commands/webhook_receiver.py
new file mode 100644
--- /dev/null
+++ b/netbox/extras/management/commands/webhook_receiver.py
@@ -0,0 +1,85 @@
+import sys
+from http.server import HTTPServer, BaseHTTPRequestHandler
+
+from django.core.management.base import BaseCommand
+
+
+request_counter = 1
+
+
+class WebhookHandler(BaseHTTPRequestHandler):
+    show_headers = True
+
+    def __getattr__(self, item):
+
+        # Return the same method for any type of HTTP request (GET, POST, etc.)
+        if item.startswith('do_'):
+            return self.do_ANY
+
+        raise AttributeError
+
+    def log_message(self, format_str, *args):
+        global request_counter
+
+        print("[{}] {} {} {}".format(
+            request_counter,
+            self.date_time_string(),
+            self.address_string(),
+            format_str % args
+        ))
+
+    def do_ANY(self):
+        global request_counter
+
+        # Send a 200 response regardless of the request content
+        self.send_response(200)
+        self.end_headers()
+        self.wfile.write(b'Webhook received!\n')
+
+        request_counter += 1
+
+        # Print the request headers to stdout
+        if self.show_headers:
+            for k, v in self.headers.items():
+                print('{}: {}'.format(k, v))
+            print()
+
+        # Print the request body (if any)
+        content_length = self.headers.get('Content-Length')
+        if content_length is not None:
+            body = self.rfile.read(int(content_length))
+            print(body.decode('utf-8'))
+        else:
+            print('(No body)')
+
+        print('------------')
+
+
+class Command(BaseCommand):
+    help = "Start a simple listener to display received HTTP requests"
+
+    default_port = 9000
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            '--port', type=int, default=self.default_port,
+            help="Optional port number (default: {})".format(self.default_port)
+        )
+        parser.add_argument(
+            "--no-headers", action='store_true', dest='no_headers',
+            help="Hide HTTP request headers"
+        )
+
+    def handle(self, *args, **options):
+        port = options['port']
+        quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
+
+        WebhookHandler.show_headers = not options['no_headers']
+
+        self.stdout.write('Listening on port http://localhost:{}. Stop with {}.'.format(port, quit_command))
+        httpd = HTTPServer(('localhost', port), WebhookHandler)
+
+        try:
+            httpd.serve_forever()
+        except KeyboardInterrupt:
+            self.stdout.write("\nExiting...")


Test patch: