forked from micolous/ircbots
-
Notifications
You must be signed in to change notification settings - Fork 1
/
ircserver.py
432 lines (369 loc) · 12.8 KB
/
ircserver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
#!/usr/bin/env python
"""
Car IRC 0.1
Copyright 2010 Michael Farrell <http://micolous.id.au/>, licensed under the AGPL3.
Preface:
This is an IRC server that I wrote on the way home (Adelaide) from Sydney in the car. It was written with only pydoc, unit tests being available for example code, RFC 1459 (the old IRC specification from 1993), a short packet capture of another IRC session, no internet access, and no working IRC server implementation to compare it to (only copies of irssi and Colloquy to test with).
You shouldn't ever use this software in a production environment, it's probably horribly broken in so many ways, and I don't plan on "finishing" the software unless I'm in a similar situation again.
I wrote this in about 7 hours, and it was extremely difficult to do given the circumstances. It was a really great challenge to have and thanks to William <http://firstyear.id.au> for coming up with it.
"""
# the car irc server of awesome.
# based on asyncore
import asynchat, socket, threading, time
CRLF = '\x0d\x0a'
RPL_WELCOME = '001'
RPL_MOTDSTART = '375'
RPL_MOTD = '372'
RPL_ENDOFMOTD = '376'
RPL_PONG = 'PONG'
RPL_NAMREPLY = '353'
RPL_ENDOFNAMES = '366'
RPL_CHANNELCREATED = '329'
RPL_WHOREPLY = '352'
RPL_ENDOFWHO = '315'
RPL_CHANNELMODEIS = '324'
RPL_TOPIC = '332'
RPL_ENDOFBANLIST = '368'
RPL_ISON = '303'
RPL_WHOISUSER = '311'
RPL_WHOISSERVER = '312'
RPL_WHOISCHANNELS = '319'
RPL_ENDOFWHOIS = '318'
ERR_NOSUCHNICK = '401'
ERR_NOSUCHCHANNEL = '403'
ERR_NICKNAMEINUSE = '433'
ERR_NOTREGISTERED = '451'
ERR_ALREADYREGISTRED = '462'
ERR_UNKNOWNCOMMAND = '421'
ERR_NEEDMOREPARAMS = '461'
CMD_USER = 'USER'
CMD_NICK = 'NICK'
CMD_QUIT = 'QUIT'
CMD_JOIN = 'JOIN'
CMD_PART = 'PART'
CMD_PING = 'PING'
CMD_WHO = 'WHO'
CMD_MODE = 'MODE'
CMD_WHOIS = 'WHOIS'
CMD_PRIVMSG = 'PRIVMSG'
CMD_TOPIC = 'TOPIC'
CMD_OPER = 'OPER'
CMD_ISON = 'ISON'
MOTD_DATA = """carirc v0.1
An IRC server that was written in a car on a trip from Sydney to Adelaide, without internet connectivity.
"""
class ClientDestroyedException(Exception):
pass
class Channel(object):
def __init__(self, name):
self.name = name
self.created = long(time.time())
self.members = []
self.modes = 'n'
self.topic = 'This is a placeholder topic by CAR IRC'
def join(self, client):
self.members.append(client)
# now send headers to them
self.send_names(client)
self.send_topic(client)
client.send(RPL_CHANNELCREATED, '%s %s' % (self.name, self.created))
# broadcast join to everyone
for user in self.members:
user.send(CMD_JOIN, ':' + self.name, src=client.get_hostmask())
def send_names(self, client):
nicks = ''
for user in self.members:
nicks += user.nickname + ' '
client.send(RPL_NAMREPLY, '= %s :%s' % (self.name, nicks))
client.send(RPL_ENDOFNAMES, self.name + ' :End of /NAMES list')
def send_who(self, client):
for user in self.members:
client.send(RPL_WHOREPLY, '%s %s %s localhost %s H :0 %s' % (self.name, user.nickname, user.hostname, user.username, user.gecos))
client.send(RPL_ENDOFWHO, self.name + ' :End of /WHO list')
def send_modes(self, client):
client.send(RPL_CHANNELMODEIS, self.name + ' +' + self.modes)
def send_topic(self, client):
client.send(RPL_TOPIC, self.name + ' :' + self.topic)
def broadcast_topic(self):
for member in self.members:
self.send_topic(member)
def send_message(self, src, msg):
# distribute the message to all except the source
for user in self.members:
if user != src:
user.send(CMD_PRIVMSG, self.name + ' :' + msg, src=src.get_hostmask())
def user_part(self, src):
# distribute the part to all members
for user in self.members:
user.send(CMD_PART, self.name + ' :', src=src.get_hostmask())
self.members.remove(user)
def send_banlist(self, client):
client.send(RPL_ENDOFBANLIST, self.name + ' :End of channel ban list')
class ClientHandler(threading.Thread):
def __init__(self, server, conn, client):
threading.Thread.__init__(self)
self.server = server
self.conn = conn
self.client = client
self.buffer = ''
self.got_user = False
self.got_nick = False
self.sent_welcome = False
self.channels = []
self.nickname = ''
self.username = ''
self.gecos = ''
self.hostname = self.client[0]
def run(self):
while True:
try:
data = self.conn.recv(1)
except socket.error:
# disconnect this client from the server
self.server.client_destroy(self)
if not data:
break
self.buffer = self.buffer + data
# check for a terminator
if CRLF in self.buffer:
# there's a terminator in the buffer, we need to split things
# find the commands that are complete
lastCrlfPos = self.buffer.rfind(CRLF)
buffer_to_process = self.buffer[:lastCrlfPos]
self.buffer = self.buffer[lastCrlfPos+len(CRLF):]
commands = buffer_to_process.split(CRLF)
for command in commands:
print "got a command: %s" % command
# now split up the command
text = None
if ':' in command:
command, text = command.split(':', 1)
command = command[:-1]
args = command.split(' ')
self.command_handler(args, text)
# now continue
def disconnect(self):
self.conn.close()
self.server.client_destroy(self)
raise ClientDestroyedException()
def command_handler(self, args, text):
print "command_handler(%s, %s)" % (args, text)
# do a cleanup
self.server.cleanup()
if args[0] == CMD_USER and not self.got_user:
# record the client userinfo
if len(args) != 4:
# it's invalid.
print "invalid %s command recieved" % CMD_USER
self.disconnect()
self.username = args[1]
self.gecos = text
self.got_user = True
elif args[0] == CMD_NICK and not self.got_nick:
# check for nick availability
nick = args[1].lower()
if self.server.nicks.has_key(nick):
# nick clash, fail
self.send(ERR_NICKNAMEINUSE, args[1] + ' :That nickname is already in use.')
else:
self.nickname = args[1]
self.server.nicks[nick] = self
self.got_nick = True
elif self.got_nick and self.got_user: # commands that are in the normal context
if args[0] == CMD_USER:
self.send(ERR_ALREADYREGISTRED, ':You may not re-register')
elif args[0] == CMD_NICK:
# check to see if the nick is taken
nick = args[1].lower()
if self.server.nicks.has_key(nick):
# nick is in use
self.send(ERR_NICKNAMEINUSE, args[1] + ' :That nickname is already in use.')
else:
# nick available
# do the switch
del self.server.nicks[self.nickname]
self.nickname = args[1]
self.server.nicks[nick] = self
elif args[0] == CMD_QUIT:
self.send(CMD_QUIT, ':Goodbye')
self.disconnect()
elif args[0] == CMD_PING:
self.send(RPL_PONG, ' '.join(args[1:]))
elif args[0] == CMD_JOIN:
# lets see if we can join the channel
if args[1][0] == '#':
# valid channel
channel = self.server.get_channel(args[1])
# add them in
channel.join(self)
self.channels.append(channel)
else:
# invalid channel
self.send(ERR_NOSUCHCHANNEL, args[1] + ' :No such channel')
elif args[0] == CMD_PART:
# part the channel
channel = self.server.get_channel(args[1])
# make sure they're a member
if self in channel.members:
channel.user_part(self)
self.channels.remove(channel)
elif args[0] == CMD_WHO:
# find out who is in the channel
channel = self.server.get_channel(args[1])
channel.send_who(self)
elif args[0] == CMD_MODE:
channel = self.server.get_channel(args[1])
# if +b is sent, send a banlist
if len(args) > 2 and args[2] == 'b':
channel.send_banlist(self)
else:
channel.send_modes(self)
elif args[0] == CMD_WHOIS:
self.whois(args[1])
elif args[0] == CMD_PRIVMSG:
if text == None:
# check to see if there's a message to send
self.send(ERR_NEEDMOREPARAMS, args[0] + ' :You need to say something, stupid')
else:
# see if it's a public or private message
if args[1][0] == '#':
# channel message
channel = self.server.get_channel(args[1])
channel.send_message(self, text)
else:
# TODO private message
# send it to the other user
target = args[1].lower()
if self.server.nicks.has_key(target):
self.server.nicks[target].send(CMD_PRIVMSG, ':' + text, src=self.get_hostmask())
else:
self.send(ERR_NOSUCHNICK, args[1] + ' :No such person exists')
elif args[0] == CMD_TOPIC:
channel = self.server.get_channel(args[1])
if text == None:
# topic request
channel.send_topic(self)
else:
# topic set
channel.topic = text
channel.broadcast_topic()
elif args[0] == CMD_ISON:
# check to see which of the people are online
print 'server.nicks = %s' % self.server.nicks
online_users = []
for query in args[1:]:
if self.server.nicks.has_key(query.lower()):
online_users.append(query)
# send reply
self.send(RPL_ISON, ':' + (' '.join(online_users)))
else:
# unknown command
self.send(ERR_UNKNOWNCOMMAND, args[0] + ' :wtf ru doin?!')
else:
# tried to send commands when not registered
self.send(ERR_NOTREGISTERED, ':You have not registered')
if self.got_nick and self.got_user and not self.sent_welcome:
# we've got all the messages we need to log in
# so lets start processing stuff
self.sent_welcome = True
self.send(RPL_WELCOME, 'Welcome to a CAR IRC Network')
# print out motd
self.send(RPL_MOTDSTART, 'Message of the day:')
for line in MOTD_DATA.split('\n'):
self.send(RPL_MOTD, '- ' + line)
self.send(RPL_ENDOFMOTD, 'End of MOTD')
def send(self, code, msg, src=None):
if src == None:
output = ':localhost %s %s %s%s' % (code, self.nickname, msg, CRLF)
else:
output = ':%s %s %s%s' % (src, code, msg, CRLF)
print 'sending %s' % output
self.conn.send(output)
def whois(self, who):
# lookup the user
who = who.lower()
if self.server.nicks.has_key(who):
# the user exists
who = self.server.nicks[who]
channellist = ''
for channel in who.channels:
channellist += channel.name + ' '
self.send(RPL_WHOISUSER, '%s %s %s * :%s' % (who.nickname, who.username, who.hostname, who.gecos))
self.send(RPL_WHOISSERVER, '%s localhost :CAR IRC LOL' % who.nickname)
self.send(RPL_WHOISCHANNELS, '%s :%s' % (who.nickname, channellist))
self.send(RPL_ENDOFWHOIS, '%s :End of /WHOIS' % who.nickname)
else:
# the user doesn't exist
self.send(ERR_NOSUCHNICK, who + ' :No such person exists')
def send_quit(self, hostmask, msg):
self.send(CMD_QUIT, ':' + msg, src=hostmask)
def get_hostmask(self):
return '%s!%s@%s' % (self.nickname, self.username, self.hostname)
class irc_server(threading.Thread):
# parameter to determine the number of bytes passed back to the
# client each send
chunk_size = 512
def __init__(self, event, host = '0.0.0.0', port = 6667):
threading.Thread.__init__(self)
self.event = event
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((host, port))
self.clients = []
self.channels = {}
self.nicks = {}
def run(self):
self.sock.listen(1)
self.event.set()
while True:
conn, client = self.sock.accept()
print "got client connection from %s" % (client, )
# dispatch a thread to handle the client
client_handler = ClientHandler(self, conn, client)
client_handler.start()
self.clients.append(client_handler)
#conn.close()
self.sock.close()
def client_destroy(self, client):
print "client disconnected on %s" % (client.client,)
self.clients.remove(client)
quit_message_recipients = []
for channel in client.channels:
channel.members.remove(client)
# copy list of users to here
for member in channel.members:
if member not in quit_message_recipients:
quit_message_recipients.append(member)
# delete from the nicklist
del self.nicks[client.nickname.lower()]
# delete client
del client
# now we have a list of people to send quit messages
# we should tell them all that Bexi likes arms.
for member in quit_message_recipients:
member.send_quit(client, 'Bexi enjoys arms and v-line buses.')
def get_channel(self, channel_name):
# see if the channel exists
if not self.channels.has_key(channel_name):
self.channels[channel_name] = Channel(channel_name)
return self.channels[channel_name]
def cleanup(self):
# do some cleanup tasks to free some memory
# hopefully python's resource counter will properly detect the object as something
# that can be disposed of
to_delete = []
for channel in self.channels.values():
if len(channel.members) == 0:
to_delete.append(channel)
for channel in to_delete:
del self.channels[channel.name]
del to_delete
def start_irc_server():
event = threading.Event()
s = irc_server(event)
s.start()
event.wait()
event.clear()
time.sleep(0.01) # Give server time to start accepting.
return s, event
if __name__ == "__main__":
start_irc_server()