diff options
-rwxr-xr-x | test/weercd.py | 470 |
1 files changed, 283 insertions, 187 deletions
diff --git a/test/weercd.py b/test/weercd.py index b64938f59..a8e1f4e02 100755 --- a/test/weercd.py +++ b/test/weercd.py @@ -35,8 +35,8 @@ # - 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. +# - 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: @@ -52,6 +52,8 @@ # Yeah, it's stable \o/ # +from __future__ import division, print_function + import argparse import os import random @@ -62,16 +64,19 @@ import socket import string import sys import time +import traceback NAME = 'weercd' -VERSION = '0.7' -DESCRIPTION = 'The WeeChat IRC testing server.' +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 = {} @@ -80,40 +85,95 @@ class Client: self.quit, self.endmsg, self.endexcept = False, '', None self.starttime = time.time() self.connect() - if not self.quit: - if self.args.file: - self.send_from_file() - else: + + 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 strrand(self, minlength=1, maxlength=50, spaces=False): - """Return string with random length and content.""" + 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)) + 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('<-- %s' % data) - msg = '%s\r\n' % data + 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 message from client.""" + """Read one IRC message from client.""" if self.args.debug: - print('--> %s' % data) + print('-->', data) if data.startswith('PING '): args = data[5:] if args[0] == ':': args = args[1:] - self.send('PONG :%s' % args) + self.send('PONG :{0}'.format(args)) elif data.startswith('NICK '): self.nick = data[5:] elif data.startswith('PART '): @@ -127,7 +187,7 @@ class Client: self.incount += 1 def read(self, timeout): - """Read data from client.""" + """Read raw data received from client.""" inr, outr, exceptr = select.select([self.sock], [], [], timeout) if inr: data = self.sock.recv(4096) @@ -144,167 +204,177 @@ class Client: self.lastbuf = data def connect(self): - """Tell client that connection is ok.""" + """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(':%s 433 * %s :Nickname is already in use.' % (NAME, self.nick)) + self.send_cmd('433', 'Nickname is already in use.', + target='* {self.nick}') self.nick = '' count -= 1 - self.send(':%s 001 %s :Welcome to the WeeChat IRC server' % (NAME, self.nick)) - self.send(':%s 002 %s :Your host is %s, running version %s' % (NAME, self.nick, NAME, VERSION)) - self.send(':%s 003 %s :Are you solid like a rock?' % (NAME, self.nick)) - self.send(':%s 004 %s :Let\'s see!' % (NAME, self.nick)) + 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 chan_randnick(self, channel): + 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)] + rnick = self.channels[channel][ + random.randint(0, len(self.channels[channel]) - 1)] return rnick - def flood(self): - """Yay, funny stuff here! Flood client!""" - if self.args.wait > 0: - print('Wait %f seconds' % self.args.wait) - time.sleep(self.args.wait) - sys.stdout.write('Flooding client..') - sys.stdout.flush() - try: - while not self.quit: - self.read(self.args.sleep) - # global actions - action = random.randint(1, 2) - if action == 1: - # join - if len(self.channels) < self.args.maxchans: - channel = '#%s' % self.strrand(1, 25) - if not channel in self.channels: - self.send(':%s!%s JOIN :%s' % (self.nick, self.addr[0], channel)) - self.send(':%s 353 %s = %s :@%s' % (NAME, self.nick, channel, self.nick)) - self.send(':%s 366 %s %s :End of /NAMES list.' % (NAME, self.nick, channel)) - self.channels[channel] = [self.nick] - elif action == 2 and 'user' in self.args.notice: - # notice for user - self.send(':%s!%s@%s NOTICE %s :%s' % (self.strrand(1, 10), self.strrand(1, 10), - self.strrand(1, 10), self.nick, - self.strrand(1, 400, True))) - # actions for each channel - for channel in self.channels: - action = random.randint(1, 50) - if action >= 1 and action <= 10: - # join - if len(self.channels[channel]) < self.args.maxnicks: - self.nicknumber += 1 - newnick = '%s%d' % (self.strrand(1, 5), self.nicknumber) - self.send(':%s!%s@%s JOIN :%s' % (newnick, self.strrand(1, 10), - self.strrand(1, 10), channel)) - self.channels[channel].append(newnick) - elif action == 11: - # part/quit - if len(self.channels[channel]) > 0: - rnick = self.chan_randnick(channel) - if rnick: - command = 'QUIT :%s' % self.strrand(1, 30) - if random.randint(1, 2) == 1: - command = 'PART %s' % channel - self.send(':%s!%s@%s %s' % (rnick, self.strrand(1, 10), - self.strrand(1, 10), command)) - self.channels[channel].remove(rnick) - elif action == 12: - # kick - if len(self.channels[channel]) > 0: - rnick1 = self.chan_randnick(channel) - rnick2 = self.chan_randnick(channel) - if rnick1 and rnick2 and rnick1 != rnick2: - self.send(':%s!%s@%s KICK %s %s :%s' % (rnick1, self.strrand(1, 10), - self.strrand(1, 10), channel, rnick2, - self.strrand(1, 50))) - self.channels[channel].remove(rnick2) - else: - # message - if len(self.channels[channel]) > 0: - rnick = self.chan_randnick(channel) - if rnick: - msg = self.strrand(1, 400, True) - if 'channel' in self.args.notice and random.randint(1, 100) == 100: - # notice for channel - self.send(':%s!%s@%s NOTICE %s :%s' % (rnick, self.strrand(1, 10), - self.strrand(1, 10), channel, msg)) - else: - # add random highlight - if random.randint(1, 100) == 100: - msg = '%s: %s' % (self.nick, msg) - action2 = random.randint(1, 50) - if action2 == 1: - # action (/me) - msg = '\x01ACTION %s\x01' % msg - elif action2 == 2: - # version - msg = '\x01VERSION\x01' - self.send(':%s!%s@%s PRIVMSG %s :%s' % (rnick, self.strrand(1, 10), - self.strrand(1, 10), channel, msg)) - # display progress - if self.outcount % 1000 == 0: - sys.stdout.write('.') - sys.stdout.flush() - except Exception as e: - if self.quit: - self.endmsg = 'quit received' - else: - self.endmsg = 'connection lost' - self.endexcept = e + def flood_self_join(self): + """Self join on a new channel.""" + channel = self.fuzzy_chan() + if channel in self.channels: return - except KeyboardInterrupt: - self.endmsg = 'interrupted' + 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.endmsg = 'quit received' + 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 send_from_file(self): + 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 - message = message.rstrip('\n') if sys.version_info < (3,): message = message.decode('UTF-8') - if not message.startswith('//'): - if not stdin: - # sleep, only if commands come from a file - self.read(self.args.sleep) - self.send(message.replace('${nick}', self.nick)) + 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 %s' % self.args.file + 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' - self.endexcept = e return except KeyboardInterrupt: self.endmsg = 'interrupted' return finally: sys.stdout.write('\n') - sys.stdout.write('%d messages sent from %s, press Enter to exit' - % (count, 'stdin' if stdin else 'file')) + 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() @@ -312,70 +382,96 @@ class Client: pass def stats(self): + """Display some statistics about data exchanged with the client.""" msgexcept = '' if self.endexcept: - msgexcept = '(%s)' % self.endexcept - print(' %s %s' % (self.endmsg, msgexcept)) + msgexcept = '({0})'.format(self.endexcept) + print(self.endmsg, msgexcept) elapsed = time.time() - self.starttime - print('Elapsed: %.1fs - packets: in:%d, out:%d (%d/s) - bytes: in:%d, out: %d (%d/s)' % ( - elapsed, self.incount, self.outcount, self.outcount / elapsed, - self.inbytes, self.outbytes, self.outbytes / elapsed)) + 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 %s' % str(self.addr)) + print('Closing connection with', self.addr) self.sock.close() -# parse command line arguments -parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, - fromfile_prefix_chars='@', - description=DESCRIPTION, - epilog='Note: the environment variable "WEERCD_OPTIONS" can be ' - 'set with some default options, and argument "@file.txt" can be ' - 'used to read some 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:]) - -print('%s %s - WeeChat IRC testing server' % (NAME, VERSION)) -print('Options: %s' % vars(args)) - -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: %s' % e) - sys.exit(1) - print('Listening on port %s (ctrl-C to exit)' % args.port) - clientsock = None - addr = None - try: - clientsock, addr = servsock.accept() - except KeyboardInterrupt: - servsock.close() - sys.exit(0) - print('Connection from %s' % str(addr)) - client = Client(clientsock, addr, args) - del client - if args.file: - break +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 |