summaryrefslogtreecommitdiff
path: root/test/weercd.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/weercd.py')
-rwxr-xr-xtest/weercd.py470
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