From eafdc1c2cca851dda202edeed1967bf380460dd3 Mon Sep 17 00:00:00 2001 From: "Mark :)" Date: Thu, 3 Oct 2019 20:39:32 +0200 Subject: [PATCH 1/5] Create discordaction.py create discordaction with webhook --- pastepwn/actions/discordaction.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 pastepwn/actions/discordaction.py diff --git a/pastepwn/actions/discordaction.py b/pastepwn/actions/discordaction.py new file mode 100644 index 0000000..fc1c003 --- /dev/null +++ b/pastepwn/actions/discordaction.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +import logging +import re +import json +from string import Template + +from pastepwn.util import Request, DictWrapper +from .basicaction import BasicAction + + +class DiscordAction(BasicAction): + """Action to send a Telegram message to a certain user or group""" + name = "DiscordAction" + + def __init__(self, webhook, custom_payload=None, template=None): + super().__init__() + self.logger = logging.getLogger(__name__) + + self.webhook = webhook + self.custom_payload = custom_payload + if template is not None: + self.template = Template(template) + else: + self.template = None + + def perform(self, paste, analyzer_name=None): + """Send a message via a Telegram bot to a specified user, without checking for errors""" + r = Request() + if self.template is None: + text = "New paste matched by analyzer '{0}' - Link: {1}".format(analyzer_name, paste.full_url) + else: + paste_dict = paste.to_dict() + paste_dict["analyzer_name"] = analyzer_name + text = self.template.safe_substitute(DictWrapper(paste_dict)) + + pasteJson = json.dumps( {"content":paste}) + + r.post(self.webhook, pasteJson) From 21ee1960829b3e4cf69f4ae8007e1009ae1db1c8 Mon Sep 17 00:00:00 2001 From: "Mark :)" Date: Thu, 3 Oct 2019 20:40:47 +0200 Subject: [PATCH 2/5] Update __init__.py --- pastepwn/actions/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pastepwn/actions/__init__.py b/pastepwn/actions/__init__.py index da99b54..297d7bc 100644 --- a/pastepwn/actions/__init__.py +++ b/pastepwn/actions/__init__.py @@ -7,5 +7,6 @@ from .genericaction import GenericAction from .databaseaction import DatabaseAction from .savejsonaction import SaveJSONAction +from .discordaction import DiscordAction -__all__ = ('BasicAction', 'SaveFileAction', 'TelegramAction', 'LogAction', 'GenericAction', 'DatabaseAction', 'SaveJSONAction') +__all__ = ('BasicAction', 'SaveFileAction', 'TelegramAction', 'LogAction', 'GenericAction', 'DatabaseAction', 'SaveJSONAction', 'DiscordAction') From 3ffc03b2b82dd8a531fb7c15debf1effc04a4abb Mon Sep 17 00:00:00 2001 From: Zeroji Date: Sun, 6 Oct 2019 16:19:18 +0200 Subject: [PATCH 3/5] Adds bot-based discord messaging Alternative to webhook-based actions, requires a bot token and channel ID in `__init__` parameters. --- pastepwn/actions/discordaction.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pastepwn/actions/discordaction.py b/pastepwn/actions/discordaction.py index fc1c003..97c63f6 100644 --- a/pastepwn/actions/discordaction.py +++ b/pastepwn/actions/discordaction.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import logging -import re import json from string import Template @@ -12,11 +11,16 @@ class DiscordAction(BasicAction): """Action to send a Telegram message to a certain user or group""" name = "DiscordAction" - def __init__(self, webhook, custom_payload=None, template=None): + def __init__(self, webhook=None, token=None, channel_id=None, custom_payload=None, template=None): super().__init__() self.logger = logging.getLogger(__name__) self.webhook = webhook + if webhook is None: + if token is None or channel_id is None: + raise ValueError('Invalid arguments: requires either webhook or token+channel_id arguments') + self.token = token + self.channel_id = channel_id self.custom_payload = custom_payload if template is not None: self.template = Template(template) @@ -32,7 +36,13 @@ def perform(self, paste, analyzer_name=None): paste_dict = paste.to_dict() paste_dict["analyzer_name"] = analyzer_name text = self.template.safe_substitute(DictWrapper(paste_dict)) - - pasteJson = json.dumps( {"content":paste}) - - r.post(self.webhook, pasteJson) + + if self.webhook is not None: + # Send to a webhook (no authentication) + url = self.webhook + else: + # Send through Discord bot API (header-based authentication) + url = 'https://discordapp.com/api/channels/{0}/messages'.format(self.channel_id) + r.headers = {'Authorization': 'Bot {}'.format(self.token)} + + r.post(url, {"content": text}) From a9ec528fbc7d9f39d29ac3f1a0bd4553c35882fb Mon Sep 17 00:00:00 2001 From: Zeroji Date: Sun, 6 Oct 2019 16:33:42 +0200 Subject: [PATCH 4/5] Remove unused import and clean up comments --- pastepwn/actions/discordaction.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pastepwn/actions/discordaction.py b/pastepwn/actions/discordaction.py index 97c63f6..399cf5e 100644 --- a/pastepwn/actions/discordaction.py +++ b/pastepwn/actions/discordaction.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import logging -import json from string import Template from pastepwn.util import Request, DictWrapper @@ -8,7 +7,7 @@ class DiscordAction(BasicAction): - """Action to send a Telegram message to a certain user or group""" + """Action to send a Discord message to a certain webhook or channel.""" name = "DiscordAction" def __init__(self, webhook=None, token=None, channel_id=None, custom_payload=None, template=None): @@ -28,7 +27,7 @@ def __init__(self, webhook=None, token=None, channel_id=None, custom_payload=Non self.template = None def perform(self, paste, analyzer_name=None): - """Send a message via a Telegram bot to a specified user, without checking for errors""" + """Send a message via Discord to a specified channel, without checking for errors""" r = Request() if self.template is None: text = "New paste matched by analyzer '{0}' - Link: {1}".format(analyzer_name, paste.full_url) From e9049770f830f2fd746dee8188410cd0759b978c Mon Sep 17 00:00:00 2001 From: Zeroji Date: Sun, 6 Oct 2019 22:07:36 +0200 Subject: [PATCH 5/5] Add Discord Gateway identification Uses websockets to open a connection to Discord's Gateway, then sends the proper identification payload so that further requests are authorized. This code runs only if sending messages fails with code 40001. --- pastepwn/actions/discordaction.py | 69 ++++++++++++++++++++++++++++++- requirements.txt | 3 +- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pastepwn/actions/discordaction.py b/pastepwn/actions/discordaction.py index 399cf5e..0ee418d 100644 --- a/pastepwn/actions/discordaction.py +++ b/pastepwn/actions/discordaction.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +import asyncio +import json import logging +import sys +import websockets from string import Template from pastepwn.util import Request, DictWrapper @@ -20,12 +24,68 @@ def __init__(self, webhook=None, token=None, channel_id=None, custom_payload=Non raise ValueError('Invalid arguments: requires either webhook or token+channel_id arguments') self.token = token self.channel_id = channel_id + self.identified = False self.custom_payload = custom_payload if template is not None: self.template = Template(template) else: self.template = None + @asyncio.coroutine + def _identify(self, ws_url): + """Connect to the Discord Gateway to identify the bot.""" + # Docs: https://discordapp.com/developers/docs/topics/gateway#connecting-to-the-gateway + # Open connection to the Discord Gateway + socket = yield from websockets.connect(ws_url + '/?v=6&encoding=json') + try: + # Receive Hello + hello_str = yield from socket.recv() + hello = json.loads(hello_str) + if hello.get('op') != 10: + self.logger.warning('[ws] Expected Hello payload but received %s', hello_str) + + # Send heartbeat and receive ACK + yield from socket.send(json.dumps({"op": 1, "d": {}})) + ack_str = yield from socket.recv() + ack = json.loads(ack_str) + if ack.get('op') != 11: + self.logger.warning('[ws] Expected Heartbeat ACK payload but received %s', ack_str) + + # Identify + payload = { + "token": self.token, + "properties": { + "$os": sys.platform, + "$browser": "pastepwn", + "$device": "pastepwn" + } + } + yield from socket.send(json.dumps({"op": 2, "d": payload})) + + # Receive READY event + ready_str = yield from socket.recv() + ready = json.loads(ready_str) + if ready.get('t') != 'READY': + self.logger.warning('[ws] Expected READY event but received %s', ready_str) + finally: + # Close websocket connection + yield from socket.close() + + def initialize_gateway(self): + """Initialize the bot token so Discord identifies it properly.""" + if self.webhook is not None: + raise NotImplementedError('Gateway initialization is only necessary for bot accounts.') + + # Call Get Gateway Bot to get the websocket URL + r = Request() + r.headers = {'Authorization': 'Bot {}'.format(self.token)} + res = json.loads(r.get('https://discordapp.com/api/gateway/bot')) + ws_url = res.get('url') + + # Start websocket client + asyncio.get_event_loop().run_until_complete(self._identify(ws_url)) + self.identified = True + def perform(self, paste, analyzer_name=None): """Send a message via Discord to a specified channel, without checking for errors""" r = Request() @@ -44,4 +104,11 @@ def perform(self, paste, analyzer_name=None): url = 'https://discordapp.com/api/channels/{0}/messages'.format(self.channel_id) r.headers = {'Authorization': 'Bot {}'.format(self.token)} - r.post(url, {"content": text}) + res = json.loads(r.post(url, {"content": text})) + + if res.get('code') == 40001 and self.webhook is None and not self.identified: + # Unauthorized access, bot token hasn't been identified to Discord Gateway + self.logger.info('Accessing Discord Gateway to initialize token') + self.initialize_gateway() + # Retry action + self.perform(paste, analyzer_name=analyzer_name) diff --git a/requirements.txt b/requirements.txt index dab93a2..c6120db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pymongo mysql-connector-python requests -python-twitter \ No newline at end of file +python-twitter +websockets>=7.0,<8 \ No newline at end of file