#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2011-2014 Sébastien Helleu # # This file is part of WeeChat, the extensible chat client. # # WeeChat is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # WeeChat is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with WeeChat. If not, see . # # # weercd - the WeeChat IRC testing server # # It can be used with any IRC client (not only WeeChat). # # In the "flood" mode, various IRC commands are sent in a short time (privmsg, # notice, join/quit, ..) to test client resistance and memory usage (to quickly # detect memory leaks, for example with client scripts). # # This script works with Python 2.x (>= 2.7) and 3.x. # # It is *STRONGLY RECOMMENDED* to connect this server with a client in a test # environment: # - for WeeChat, another home with: `weechat --dir /tmp/weechat` # - on a test machine, because CPU will be used a lot by client to display # messages from weercd # - if possible locally (ie server and client on same machine), to speed up # data exchange between server and client. # # Instructions to use this server with WeeChat: # 1. open a terminal and run server: # python weercd.py # 2. open another terminal and run WeeChat with home in /tmp: # weechat --dir /tmp/weechat # 3. optional: install script(s) (/script install ...) # 4. add server and connect to it: # /server add weercd 127.0.0.1/7777 # /connect weercd # 5. wait some months..... # WeeChat still not crashed and does not use 200 TB of RAM ? # Yeah, it's stable \o/ # from __future__ import division, print_function import argparse import os import random import re import select import shlex import socket import string import sys import time import traceback NAME = 'weercd' VERSION = '0.8' class Client: def __init__(self, sock, addr, args, **kwargs): self.sock, self.addr = sock, addr self.args = args self.name = NAME self.version = VERSION self.nick = '' self.nicknumber = 0 self.channels = {} self.lastbuf = '' self.incount, self.outcount, self.inbytes, self.outbytes = 0, 0, 0, 0 self.quit, self.endmsg, self.endexcept = False, '', None self.starttime = time.time() self.connect() def run(self): """Execute the action asked for the client.""" if self.quit: return # send commands from file (which can be stdin) if self.args.file: self.send_file() return # flood the client if self.args.wait > 0: print('Waiting', self.args.wait, 'seconds') time.sleep(self.args.wait) sys.stdout.write('Flooding client..') sys.stdout.flush() try: while not self.quit: self.flood() except Exception as e: if self.quit: self.endmsg = 'quit received' else: self.endmsg = 'connection lost' self.endexcept = e except KeyboardInterrupt: self.endmsg = 'interrupted' else: self.endmsg = 'quit received' def fuzzy_str(self, minlength=1, maxlength=50, spaces=False): """Return a fuzzy string (random length and content).""" length = random.randint(minlength, maxlength) strspace = '' if spaces: strspace = ' ' return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits + strspace) for x in range(length)) def fuzzy_host(self): """Return a fuzzy host name.""" return '{0}@{1}'.format(self.fuzzy_str(1, 10), self.fuzzy_str(1, 10)) def fuzzy_nick(self, with_number=False): """Return a fuzzy nick name.""" if with_number: self.nicknumber += 1 return '{0}{1}'.format(self.fuzzy_str(1, 5), self.nicknumber) else: return self.fuzzy_str(1, 10) def fuzzy_chan(self): """Return a fuzzy channel name.""" return '#{0}'.format(self.fuzzy_str(1, 25)) def send(self, data): """Send one message to client.""" if self.args.debug: print('<--', data) msg = data + '\r\n' self.outbytes += len(msg) self.sock.send(msg.encode('UTF-8')) self.outcount += 1 def send_cmd(self, cmd, data, nick='{self.name}', host='', target='{self.nick}'): """Send an IRC command to the client.""" self.send(':{0}{1}{2} {3}{4}{5}{6}{7}' ''.format(nick, '!' if host else '', host, cmd, ' ' if target else '', target, ' :' if data else '', data).format(self=self)) def recv(self, data): """Read one IRC message from client.""" if self.args.debug: print('-->', data) if data.startswith('PING '): args = data[5:] if args[0] == ':': args = args[1:] self.send('PONG :{0}'.format(args)) elif data.startswith('NICK '): self.nick = data[5:] elif data.startswith('PART '): m = re.search('^PART :?(#[^ ]+)', data) if m: channel = m.group(1) if channel in self.channels: del self.channels[channel] elif data.startswith('QUIT '): self.quit = True self.incount += 1 def read(self, timeout): """Read raw data received from client.""" inr, outr, exceptr = select.select([self.sock], [], [], timeout) if inr: data = self.sock.recv(4096) if data: data = data.decode('UTF-8') self.inbytes += len(data) data = self.lastbuf + data while True: pos = data.find('\r\n') if pos < 0: break self.recv(data[0:pos]) data = data[pos + 2:] self.lastbuf = data def connect(self): """Inform the client that the connection is OK.""" try: count = self.args.nickused while self.nick == '': self.read(0.1) if self.nick and count > 0: self.send_cmd('433', 'Nickname is already in use.', target='* {self.nick}') self.nick = '' count -= 1 self.send_cmd('001', 'Welcome to the WeeChat IRC server') self.send_cmd('002', 'Your host is {self.name}, running version ' '{self.version}') self.send_cmd('003', 'Are you solid like a rock?') self.send_cmd('004', 'Let\'s see!') except KeyboardInterrupt: self.quit = True self.endmsg = 'interrupted' return def channel_random_nick(self, channel): """Return a random nick of a channel.""" if len(self.channels[channel]) < 2: return None rnick = self.nick while rnick == self.nick: rnick = self.channels[channel][ random.randint(0, len(self.channels[channel]) - 1)] return rnick def flood_self_join(self): """Self join on a new channel.""" channel = self.fuzzy_chan() if channel in self.channels: return self.send_cmd('JOIN', channel, nick=self.nick, host=self.addr[0], target='') self.send_cmd('353', '@{self.nick}', target='{0} = {1}'.format(self.nick, channel)) self.send_cmd('366', 'End of /NAMES list.', target='{0} {1}'.format(self.nick, channel)) self.channels[channel] = [self.nick] def flood_user_notice(self): """Notice for the user.""" self.send_cmd('NOTICE', self.fuzzy_str(1, 400, spaces=True), nick=self.fuzzy_nick(), host=self.fuzzy_host()) def flood_channel_join(self, channel): """Join of a user in a channel.""" if len(self.channels[channel]) >= self.args.maxnicks: return newnick = self.fuzzy_nick(with_number=True) self.send_cmd('JOIN', channel, nick=newnick, host=self.fuzzy_host(), target='') self.channels[channel].append(newnick) def flood_channel_part(self, channel): """Part or quit of a user in a channel.""" if len(self.channels[channel]) == 0: return rnick = self.channel_random_nick(channel) if not rnick: return if random.randint(1, 2) == 1: self.send_cmd('PART', channel, nick=rnick, host=self.fuzzy_host(), target='') else: self.send_cmd('QUIT', self.fuzzy_str(1, 30), nick=rnick, host=self.fuzzy_host(), target='') self.channels[channel].remove(rnick) def flood_channel_kick(self, channel): """Kick of a user in a channel.""" if len(self.channels[channel]) == 0: return rnick1 = self.channel_random_nick(channel) rnick2 = self.channel_random_nick(channel) if rnick1 and rnick2 and rnick1 != rnick2: self.send_cmd('KICK', self.fuzzy_str(1, 50), nick=rnick1, host=self.fuzzy_host(), target='{0} {1}'.format(channel, rnick2)) self.channels[channel].remove(rnick2) def flood_channel_message(self, channel): """Message from a user in a channel.""" if len(self.channels[channel]) == 0: return rnick = self.channel_random_nick(channel) if not rnick: return msg = self.fuzzy_str(1, 400, spaces=True) if 'channel' in self.args.notice and random.randint(1, 100) == 100: # notice for channel self.send_cmd('NOTICE', msg, nick=rnick, host=self.fuzzy_host(), target=channel) else: # add random highlight if random.randint(1, 100) == 100: msg = '{0}: {1}'.format(self.nick, msg) action2 = random.randint(1, 50) if action2 == 1: # CTCP action (/me) msg = '\x01ACTION {0}\x01'.format(msg) elif action2 == 2: # CTCP version msg = '\x01VERSION\x01' self.send_cmd('PRIVMSG', msg, nick=rnick, host=self.fuzzy_host(), target=channel) def flood(self): """Yay, funny stuff here! Flood the client!""" self.read(self.args.sleep) # global actions action = random.randint(1, 2) if action == 1 and len(self.channels) < self.args.maxchans: self.flood_self_join() elif action == 2 and 'user' in self.args.notice: self.flood_user_notice() # actions for each channel for channel in self.channels: action = random.randint(1, 50) if 1 <= action <= 10: self.flood_channel_join(channel) elif action == 11: self.flood_channel_part(channel) elif action == 12: self.flood_channel_kick(channel) else: self.flood_channel_message(channel) # display progress if self.outcount % 1000 == 0: sys.stdout.write('.') sys.stdout.flush() def send_file(self): """Send messages from a file to client.""" stdin = self.args.file == sys.stdin count = 0 self.read(0.2) try: while True: # display the prompt if we are reading in stdin if stdin: sys.stdout.write('Message to send to client: ') sys.stdout.flush() message = self.args.file.readline() if not message: break if sys.version_info < (3,): message = message.decode('UTF-8') message = message.rstrip('\n') if message and not message.startswith('//'): self.send(message.format(self=self)) count += 1 self.read(0.1 if stdin else self.args.sleep) except IOError as e: self.endmsg = 'unable to read file {0}'.format(self.args.file) self.endexcept = e return except Exception as e: traceback.print_exc() self.endmsg = 'connection lost' return except KeyboardInterrupt: self.endmsg = 'interrupted' return finally: sys.stdout.write('\n') sys.stdout.write('{0} messages sent from {1}, press Enter to exit' .format(count, 'stdin' if stdin else 'file')) sys.stdout.flush() try: sys.stdin.readline() except: pass def stats(self): """Display some statistics about data exchanged with the client.""" msgexcept = '' if self.endexcept: msgexcept = '({0})'.format(self.endexcept) print(self.endmsg, msgexcept) elapsed = time.time() - self.starttime countrate = self.outcount / elapsed bytesrate = self.outbytes / elapsed print('Elapsed: {elapsed:.1f}s - ' 'packets: in:{self.incount}, out:{self.outcount} ' '({countrate:.0f}/s) - ' 'bytes: in:{self.inbytes}, out: {self.outbytes} ' '({bytesrate:.0f}/s)' ''.format(self=self, elapsed=elapsed, countrate=countrate, bytesrate=bytesrate)) if self.endmsg == 'connection lost': print('Uh-oh! No quit received, client has crashed? Ahah \o/') def __del__(self): self.stats() print('Closing connection with', self.addr) self.sock.close() if __name__ == "__main__": # parse command line arguments parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, fromfile_prefix_chars='@', description='The WeeChat IRC testing server.', epilog='Note: the environment variable "WEERCD_OPTIONS" can be ' 'set with default options. Argument "@file.txt" can be used to read ' 'default options in a file.') parser.add_argument('-H', '--host', help='host for socket bind') parser.add_argument('-p', '--port', type=int, default=7777, help='port for socket bind') parser.add_argument('-f', '--file', type=argparse.FileType('r'), help='send messages from file, instead of flooding ' 'the client (use "-" for stdin)') parser.add_argument('-c', '--maxchans', type=int, default=5, help='max number of channels to join') parser.add_argument('-n', '--maxnicks', type=int, default=100, help='max number of nicks per channel') parser.add_argument('-u', '--nickused', type=int, default=0, help='send 433 (nickname already in use) this number ' 'of times before accepting nick') parser.add_argument('-N', '--notice', metavar='NOTICE_TYPE', choices=['user', 'channel'], default=['user', 'channel'], nargs='*', help='notices to send: "user" (to user), "channel" ' '(to channel)') parser.add_argument('-s', '--sleep', type=float, default=0, help='sleep for select: delay between 2 messages sent ' 'to client (float, in seconds)') parser.add_argument('-w', '--wait', type=float, default=0, help='time to wait before flooding client (float, ' 'in seconds)') parser.add_argument('-d', '--debug', action='store_true', help='debug output') parser.add_argument('-v', '--version', action='version', version=VERSION) args = parser.parse_args(shlex.split(os.getenv('WEERCD_OPTIONS') or '') + sys.argv[1:]) # welcome message, with options print(NAME, VERSION, '- WeeChat IRC testing server') print('Options:', vars(args)) # main loop while True: servsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: servsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) servsock.bind((args.host or '', args.port)) servsock.listen(1) except Exception as e: print('Socket error: {0}'.format(e)) sys.exit(1) print('Listening on port', args.port, '(ctrl-C to exit)') clientsock = None addr = None try: clientsock, addr = servsock.accept() except KeyboardInterrupt: servsock.close() sys.exit(0) print('Connection from', addr) client = Client(clientsock, addr, args) client.run() del client # no loop if message were sent from a file if args.file: break