Alcuin

Below is an updated version of the IRC emulator using UDP for links between servers that I published recently. In this update:

  • A new name: alcuin
  • Refactored into multiple files for ease of reading
  • Messages are now sent between servers with hashes attached so that garbage messages can be easily discarded
  • Apparently JOIN/PART messages are actually supported, your server just has to be running in time to catch them
  • Added a perfunctory amount of documentation in README.txt

Download the source.

1 diff --git a/Makefile b/Makefile
2 new file mode 100644
3 index 0000000..1db3ff4
4 --- /dev/null
5 +++ b/Makefile
6 @@ -0,0 +1,23 @@
7 +VERSION := $(shell sed -ne 's/^VERSION = "\(.*\)"/\1/p' lib/server.py)
8 +
9 +DISTFILES = alcuin config.py.example README.txt lib
10 +
11 +all:
12 + echo "Nothing to do."
13 +
14 +dist:
15 + mkdir alcuin-$(VERSION)
16 + cp -r $(DISTFILES) alcuin-$(VERSION)
17 + tar cvzf alcuin-$(VERSION).tar.gz alcuin-$(VERSION)
18 + rm -rf alcuin-$(VERSION)
19 +
20 +clean:
21 + rm -rf genesis.vdiff genesis.vdiff.escaped alcuin-$(VERSION)
22 + find . -name "*.swp" -delete
23 + find . -name "*.pyc" -delete
24 +
25 +genesis:
26 + git show --pretty="format:" -1 HEAD > genesis.vdiff
27 +
28 +escaped-genesis:
29 + git show --pretty="format:" -1 HEAD | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g; s/((/\&lpar;\&lpar;/g; s/))/\&rpar;\&rpar;/g; s/\[([0-9])\[/\&lsqb;$1\&lsqb;/g; s/]]/\&rsqb;\&rsqb;/g' > genesis.vdiff.escaped
30 diff --git a/README.txt b/README.txt
31 new file mode 100644
32 index 0000000..2d5ff88
33 --- /dev/null
34 +++ b/README.txt
35 @@ -0,0 +1,25 @@
36 +Alcuin implements an IRC server with a udp client built in
37 +such that it can connect to other alcuin servers via a
38 +gossip network. It is a work in progress and much is
39 +yet to be completed including but not limited to:
40 +
41 +- gossip style message forwarding
42 +- message deduplication
43 +- symmetric encryption of messages
44 +- support for broadcasting several important IRC commands
45 + over the gossip net.
46 +- mitigation of hash length extension attacks
47 +
48 +GETTING STARTED
49 +
50 +1. Copy config.py.example to config.py (nothing in the config file is
51 +used yet, but alcuin will crash if it doesn't exist).
52 +2. Launch alcuin with something like the following command:
53 +./alcuin --verbose --port=6668 --peers=206.189.163.145
54 +
55 +NOTES FOR DIFF/PATCH N00B5
56 +
57 +To apply the genesis patch (or any patch) to the current directory
58 +and recreate the directory structure of the original:
59 +
60 +patch -p1 -ruN < <wherever>/genesis.vdiff
61 diff --git a/alcuin b/alcuin
62 new file mode 100755
63 index 0000000..bbf9ed4
64 --- /dev/null
65 +++ b/alcuin
66 @@ -0,0 +1,146 @@
67 +#! /usr/bin/env python
68 +
69 +import os
70 +import re
71 +import select
72 +import socket
73 +import string
74 +import sys
75 +import tempfile
76 +import time
77 +from lib.server import VERSION
78 +from lib.server import Server
79 +from lib.peer import Peer
80 +from datetime import datetime
81 +from optparse import OptionParser
82 +import config as cfg
83 +
84 +
85 +def main(argv):
86 + op = OptionParser(
87 + version=VERSION,
88 + description="alcuin is a small and limited IRC server emulator for gossip networks.")
89 + op.add_option(
90 + "-d", "--daemon",
91 + action="store_true",
92 + help="fork and become a daemon")
93 + op.add_option(
94 + "--debug",
95 + action="store_true",
96 + help="print debug messages to stdout")
97 + op.add_option(
98 + "--listen",
99 + metavar="X",
100 + help="listen on specific IP address X")
101 + op.add_option(
102 + "--logdir",
103 + metavar="X",
104 + help="store channel log in directory X")
105 + op.add_option(
106 + "--motd",
107 + metavar="X",
108 + help="display file X as message of the day")
109 + op.add_option(
110 + "-s", "--ssl-pem-file",
111 + metavar="FILE",
112 + help="enable SSL and use FILE as the .pem certificate+key")
113 + op.add_option(
114 + "-p", "--password",
115 + metavar="X",
116 + help="require connection password X; default: no password")
117 + op.add_option(
118 + "--ports",
119 + metavar="X",
120 + help="listen to ports X (a list separated by comma or whitespace);"
121 + " default: 6667 or 6697 if SSL is enabled")
122 + op.add_option(
123 + "--udp-port",
124 + metavar="X",
125 + help="listen for UDP packets on X;"
126 + " default: 7778")
127 + op.add_option(
128 + "--peers",
129 + metavar="X",
130 + help="Broadcast to X (a list of IP addresses separated by comma or whitespace)")
131 + op.add_option(
132 + "--statedir",
133 + metavar="X",
134 + help="save persistent channel state (topic, key) in directory X")
135 + op.add_option(
136 + "--verbose",
137 + action="store_true",
138 + help="be verbose (print some progress messages to stdout)")
139 + if os.name == "posix":
140 + op.add_option(
141 + "--chroot",
142 + metavar="X",
143 + help="change filesystem root to directory X after startup"
144 + " (requires root)")
145 + op.add_option(
146 + "--setuid",
147 + metavar="U[:G]",
148 + help="change process user (and optionally group) after startup"
149 + " (requires root)")
150 +
151 + (options, args) = op.parse_args(argv[1:])
152 + if options.debug:
153 + options.verbose = True
154 + if options.ports is None:
155 + if options.ssl_pem_file is None:
156 + options.ports = "6667"
157 + else:
158 + options.ports = "6697"
159 + if options.peers is None:
160 + options.peers = ""
161 + if options.udp_port is None:
162 + options.udp_port = 7778
163 + else:
164 + options.udp_port = int(options.udp_port)
165 + if options.chroot:
166 + if os.getuid() != 0:
167 + op.error("Must be root to use --chroot")
168 + if options.setuid:
169 + from pwd import getpwnam
170 + from grp import getgrnam
171 + if os.getuid() != 0:
172 + op.error("Must be root to use --setuid")
173 + matches = options.setuid.split(":")
174 + if len(matches) == 2:
175 + options.setuid = (getpwnam(matches[0]).pw_uid,
176 + getgrnam(matches[1]).gr_gid)
177 + elif len(matches) == 1:
178 + options.setuid = (getpwnam(matches[0]).pw_uid,
179 + getpwnam(matches[0]).pw_gid)
180 + else:
181 + op.error("Specify a user, or user and group separated by a colon,"
182 + " e.g. --setuid daemon, --setuid nobody:nobody")
183 + if (os.getuid() == 0 or os.getgid() == 0) and not options.setuid:
184 + op.error("Running this service as root is not recommended. Use the"
185 + " --setuid option to switch to an unprivileged account after"
186 + " startup. If you really intend to run as root, use"
187 + " \"--setuid root\".")
188 +
189 + ports = []
190 + for port in re.split(r"[,\s]+", options.ports):
191 + try:
192 + ports.append(int(port))
193 + except ValueError:
194 + op.error("bad port: %r" % port)
195 + options.ports = ports
196 + peers = []
197 + for peer in re.split(r"[,\s]+", options.peers):
198 + try:
199 + peers.append(Peer(peer))
200 + except ValueError:
201 + op.error("bad peer ip: %r" % peer)
202 + options.peers = peers
203 + server = Server(options)
204 + if options.daemon:
205 + server.daemonize()
206 + try:
207 + server.start()
208 + except KeyboardInterrupt:
209 + server.print_error("Interrupted.")
210 +
211 +
212 +main(sys.argv)
213 diff --git a/config.py.example b/config.py.example
214 new file mode 100644
215 index 0000000..f9adc62
216 --- /dev/null
217 +++ b/config.py.example
218 @@ -0,0 +1,4 @@
219 +secret = "SEEKRIT"
220 +peer_secrets = {
221 + "10.0.0.1":"K33P-0U7!"
222 +}
223 diff --git a/lib/__init__.py b/lib/__init__.py
224 new file mode 100644
225 index 0000000..d2e75fb
226 --- /dev/null
227 +++ b/lib/__init__.py
228 @@ -0,0 +1 @@
229 +# This file can't be empty otherwise diff won't see it.
230 diff --git a/lib/channel.py b/lib/channel.py
231 new file mode 100644
232 index 0000000..5086804
233 --- /dev/null
234 +++ b/lib/channel.py
235 @@ -0,0 +1,60 @@
236 +class Channel(object):
237 + def __init__(self, server, name):
238 + self.server = server
239 + self.name = name
240 + self.members = set()
241 + self._topic = ""
242 + self._key = None
243 + if self.server.statedir:
244 + self._state_path = "%s/%s" % (
245 + self.server.statedir,
246 + name.replace("_", "__").replace("/", "_"))
247 + self._read_state()
248 + else:
249 + self._state_path = None
250 +
251 + def add_member(self, client):
252 + self.members.add(client)
253 +
254 + def get_topic(self):
255 + return self._topic
256 +
257 + def set_topic(self, value):
258 + self._topic = value
259 + self._write_state()
260 +
261 + topic = property(get_topic, set_topic)
262 +
263 + def get_key(self):
264 + return self._key
265 +
266 + def set_key(self, value):
267 + self._key = value
268 + self._write_state()
269 +
270 + key = property(get_key, set_key)
271 +
272 + def remove_client(self, client):
273 + self.members.discard(client)
274 + if not self.members:
275 + self.server.remove_channel(self)
276 +
277 + def _read_state(self):
278 + if not (self._state_path and os.path.exists(self._state_path)):
279 + return
280 + data = {}
281 + exec(open(self._state_path), {}, data)
282 + self._topic = data.get("topic", "")
283 + self._key = data.get("key")
284 +
285 + def _write_state(self):
286 + if not self._state_path:
287 + return
288 + (fd, path) = tempfile.mkstemp(dir=os.path.dirname(self._state_path))
289 + fp = os.fdopen(fd, "w")
290 + fp.write("topic = %r\n" % self.topic)
291 + fp.write("key = %r\n" % self.key)
292 + fp.close()
293 + os.rename(path, self._state_path)
294 +
295 +
296 diff --git a/lib/client.py b/lib/client.py
297 new file mode 100644
298 index 0000000..cfc5331
299 --- /dev/null
300 +++ b/lib/client.py
301 @@ -0,0 +1,548 @@
302 +import time
303 +import sys
304 +import re
305 +import string
306 +from lib.server import VERSION
307 +from lib.infosec import Infosec
308 +from funcs import *
309 +
310 +class Client(object):
311 + __linesep_regexp = re.compile(r"\r?\n")
312 + # The RFC limit for nicknames is 9 characters, but what the heck.
313 + __valid_nickname_regexp = re.compile(
314 + r"^[][\`_^{|}A-Za-z][][\`_^{|}A-Za-z0-9-]{0,50}$")
315 + __valid_channelname_regexp = re.compile(
316 + r"^[&#+!][^\x00\x07\x0a\x0d ,:]{0,50}$")
317 +
318 + def __init__(self, server, socket):
319 + self.server = server
320 + self.socket = socket
321 + self.channels = {} # irc_lower(Channel name) --> Channel
322 + self.nickname = None
323 + self.user = None
324 + self.realname = None
325 + (self.host, self.port) = socket.getpeername()
326 + self.__timestamp = time.time()
327 + self.__readbuffer = ""
328 + self.__writebuffer = ""
329 + self.__sent_ping = False
330 + self.infosec = Infosec()
331 + if self.server.password:
332 + self.__handle_command = self.__pass_handler
333 + else:
334 + self.__handle_command = self.__registration_handler
335 +
336 + def get_prefix(self):
337 + return "%s!%s@%s" % (self.nickname, self.user, self.host)
338 + prefix = property(get_prefix)
339 +
340 + def check_aliveness(self):
341 + now = time.time()
342 + if self.__timestamp + 180 < now:
343 + self.disconnect("ping timeout")
344 + return
345 + if not self.__sent_ping and self.__timestamp + 90 < now:
346 + if self.__handle_command == self.__command_handler:
347 + # Registered.
348 + self.message("PING :%s" % self.server.name)
349 + self.__sent_ping = True
350 + else:
351 + # Not registered.
352 + self.disconnect("ping timeout")
353 +
354 + def write_queue_size(self):
355 + return len(self.__writebuffer)
356 +
357 + def __parse_read_buffer(self):
358 + lines = self.__linesep_regexp.split(self.__readbuffer)
359 + self.__readbuffer = lines[-1]
360 + lines = lines[:-1]
361 + for line in lines:
362 + if not line:
363 + # Empty line. Ignore.
364 + continue
365 + x = line.split(" ", 1)
366 + command = x[0].upper()
367 + if len(x) == 1:
368 + arguments = []
369 + else:
370 + if len(x[1]) > 0 and x[1][0] == ":":
371 + arguments = [x[1][1:]]
372 + else:
373 + y = string.split(x[1], " :", 1)
374 + arguments = string.split(y[0])
375 + if len(y) == 2:
376 + arguments.append(y[1])
377 + self.__handle_command(command, arguments)
378 +
379 + def __pass_handler(self, command, arguments):
380 + server = self.server
381 + if command == "PASS":
382 + if len(arguments) == 0:
383 + self.reply_461("PASS")
384 + else:
385 + if arguments[0].lower() == server.password:
386 + self.__handle_command = self.__registration_handler
387 + else:
388 + self.reply("464 :Password incorrect")
389 + elif command == "QUIT":
390 + self.disconnect("Client quit")
391 + return
392 +
393 + def __registration_handler(self, command, arguments):
394 + server = self.server
395 + if command == "NICK":
396 + if len(arguments) < 1:
397 + self.reply("431 :No nickname given")
398 + return
399 + nick = arguments[0]
400 + if server.get_client(nick):
401 + self.reply("433 * %s :Nickname is already in use" % nick)
402 + elif not self.__valid_nickname_regexp.match(nick):
403 + self.reply("432 * %s :Erroneous nickname" % nick)
404 + else:
405 + self.nickname = nick
406 + server.client_changed_nickname(self, None)
407 + elif command == "USER":
408 + if len(arguments) < 4:
409 + self.reply_461("USER")
410 + return
411 + self.user = arguments[0]
412 + self.realname = arguments[3]
413 + elif command == "QUIT":
414 + self.disconnect("Client quit")
415 + return
416 + if self.nickname and self.user:
417 + self.reply("001 %s :Hi, welcome to IRC" % self.nickname)
418 + self.reply("002 %s :Your host is %s, running version miniircd-%s"
419 + % (self.nickname, server.name, VERSION))
420 + self.reply("003 %s :This server was created sometime"
421 + % self.nickname)
422 + self.reply("004 %s :%s miniircd-%s o o"
423 + % (self.nickname, server.name, VERSION))
424 + self.send_lusers()
425 + self.send_motd()
426 + self.__handle_command = self.__command_handler
427 +
428 + def __command_handler(self, command, arguments):
429 + def away_handler():
430 + pass
431 +
432 + def ison_handler():
433 + if len(arguments) < 1:
434 + self.reply_461("ISON")
435 + return
436 + nicks = arguments
437 + online = [n for n in nicks if server.get_client(n)]
438 + self.reply("303 %s :%s" % (self.nickname, " ".join(online)))
439 +
440 + def join_handler():
441 + if len(arguments) < 1:
442 + self.reply_461("JOIN")
443 + return
444 + if arguments[0] == "0":
445 + for (channelname, channel) in self.channels.items():
446 + self.message_channel(channel, "PART", channelname, True)
447 + self.channel_log(channel, "left", meta=True)
448 + server.remove_member_from_channel(self, channelname)
449 + self.channels = {}
450 + return
451 + channelnames = arguments[0].split(",")
452 + if len(arguments) > 1:
453 + keys = arguments[1].split(",")
454 + else:
455 + keys = []
456 + keys.extend((len(channelnames) - len(keys)) * [None])
457 + for (i, channelname) in enumerate(channelnames):
458 + if irc_lower(channelname) in self.channels:
459 + continue
460 + if not valid_channel_re.match(channelname):
461 + self.reply_403(channelname)
462 + continue
463 + channel = server.get_channel(channelname)
464 + if channel.key is not None and channel.key != keys[i]:
465 + self.reply(
466 + "475 %s %s :Cannot join channel (+k) - bad key"
467 + % (self.nickname, channelname))
468 + continue
469 + channel.add_member(self)
470 + self.channels[irc_lower(channelname)] = channel
471 + self.message_channel(channel, "JOIN", channelname, True)
472 + self.channel_log(channel, "joined", meta=True)
473 + if channel.topic:
474 + self.reply("332 %s %s :%s"
475 + % (self.nickname, channel.name, channel.topic))
476 + else:
477 + self.reply("331 %s %s :No topic is set"
478 + % (self.nickname, channel.name))
479 + self.reply("353 %s = %s :%s"
480 + % (self.nickname,
481 + channelname,
482 + " ".join(sorted(x.nickname
483 + for x in channel.members))))
484 + self.reply("366 %s %s :End of NAMES list"
485 + % (self.nickname, channelname))
486 +
487 + def list_handler():
488 + if len(arguments) < 1:
489 + channels = server.channels.values()
490 + else:
491 + channels = []
492 + for channelname in arguments[0].split(","):
493 + if server.has_channel(channelname):
494 + channels.append(server.get_channel(channelname))
495 + channels.sort(key=lambda x: x.name)
496 + for channel in channels:
497 + self.reply("322 %s %s %d :%s"
498 + % (self.nickname, channel.name,
499 + len(channel.members), channel.topic))
500 + self.reply("323 %s :End of LIST" % self.nickname)
501 +
502 + def lusers_handler():
503 + self.send_lusers()
504 +
505 + def mode_handler():
506 + if len(arguments) < 1:
507 + self.reply_461("MODE")
508 + return
509 + targetname = arguments[0]
510 + if server.has_channel(targetname):
511 + channel = server.get_channel(targetname)
512 + if len(arguments) < 2:
513 + if channel.key:
514 + modes = "+k"
515 + if irc_lower(channel.name) in self.channels:
516 + modes += " %s" % channel.key
517 + else:
518 + modes = "+"
519 + self.reply("324 %s %s %s"
520 + % (self.nickname, targetname, modes))
521 + return
522 + flag = arguments[1]
523 + if flag == "+k":
524 + if len(arguments) < 3:
525 + self.reply_461("MODE")
526 + return
527 + key = arguments[2]
528 + if irc_lower(channel.name) in self.channels:
529 + channel.key = key
530 + self.message_channel(
531 + channel, "MODE", "%s +k %s" % (channel.name, key),
532 + True)
533 + self.channel_log(
534 + channel, "set channel key to %s" % key, meta=True)
535 + else:
536 + self.reply("442 %s :You're not on that channel"
537 + % targetname)
538 + elif flag == "-k":
539 + if irc_lower(channel.name) in self.channels:
540 + channel.key = None
541 + self.message_channel(
542 + channel, "MODE", "%s -k" % channel.name,
543 + True)
544 + self.channel_log(
545 + channel, "removed channel key", meta=True)
546 + else:
547 + self.reply("442 %s :You're not on that channel"
548 + % targetname)
549 + else:
550 + self.reply("472 %s %s :Unknown MODE flag"
551 + % (self.nickname, flag))
552 + elif targetname == self.nickname:
553 + if len(arguments) == 1:
554 + self.reply("221 %s +" % self.nickname)
555 + else:
556 + self.reply("501 %s :Unknown MODE flag" % self.nickname)
557 + else:
558 + self.reply_403(targetname)
559 +
560 + def motd_handler():
561 + self.send_motd()
562 +
563 + def nick_handler():
564 + if len(arguments) < 1:
565 + self.reply("431 :No nickname given")
566 + return
567 + newnick = arguments[0]
568 + client = server.get_client(newnick)
569 + if newnick == self.nickname:
570 + pass
571 + elif client and client is not self:
572 + self.reply("433 %s %s :Nickname is already in use"
573 + % (self.nickname, newnick))
574 + elif not self.__valid_nickname_regexp.match(newnick):
575 + self.reply("432 %s %s :Erroneous Nickname"
576 + % (self.nickname, newnick))
577 + else:
578 + for x in self.channels.values():
579 + self.channel_log(
580 + x, "changed nickname to %s" % newnick, meta=True)
581 + oldnickname = self.nickname
582 + self.nickname = newnick
583 + server.client_changed_nickname(self, oldnickname)
584 + self.message_related(
585 + ":%s!%s@%s NICK %s"
586 + % (oldnickname, self.user, self.host, self.nickname),
587 + True)
588 +
589 + def notice_and_privmsg_handler():
590 + if len(arguments) == 0:
591 + self.reply("411 %s :No recipient given (%s)"
592 + % (self.nickname, command))
593 + return
594 + if len(arguments) == 1:
595 + self.reply("412 %s :No text to send" % self.nickname)
596 + return
597 + targetname = arguments[0]
598 + message = arguments[1]
599 + client = server.get_client(targetname)
600 +
601 + if client:
602 + client.message(":%s %s %s :%s"
603 + % (self.prefix, command, targetname, message))
604 + elif server.has_channel(targetname):
605 + channel = server.get_channel(targetname)
606 + self.message_channel(
607 + channel, command, "%s :%s" % (channel.name, message))
608 + self.channel_log(channel, message)
609 + else:
610 + self.reply("401 %s %s :No such nick/channel"
611 + % (self.nickname, targetname))
612 +
613 + def part_handler():
614 + if len(arguments) < 1:
615 + self.reply_461("PART")
616 + return
617 + if len(arguments) > 1:
618 + partmsg = arguments[1]
619 + else:
620 + partmsg = self.nickname
621 + for channelname in arguments[0].split(","):
622 + if not valid_channel_re.match(channelname):
623 + self.reply_403(channelname)
624 + elif not irc_lower(channelname) in self.channels:
625 + self.reply("442 %s %s :You're not on that channel"
626 + % (self.nickname, channelname))
627 + else:
628 + channel = self.channels[irc_lower(channelname)]
629 + self.message_channel(
630 + channel, "PART", "%s :%s" % (channelname, partmsg),
631 + True)
632 + self.channel_log(channel, "left (%s)" % partmsg, meta=True)
633 + del self.channels[irc_lower(channelname)]
634 + server.remove_member_from_channel(self, channelname)
635 +
636 + def ping_handler():
637 + if len(arguments) < 1:
638 + self.reply("409 %s :No origin specified" % self.nickname)
639 + return
640 + self.reply("PONG %s :%s" % (server.name, arguments[0]))
641 +
642 + def pong_handler():
643 + pass
644 +
645 + def quit_handler():
646 + if len(arguments) < 1:
647 + quitmsg = self.nickname
648 + else:
649 + quitmsg = arguments[0]
650 + self.disconnect(quitmsg)
651 +
652 + def topic_handler():
653 + if len(arguments) < 1:
654 + self.reply_461("TOPIC")
655 + return
656 + channelname = arguments[0]
657 + channel = self.channels.get(irc_lower(channelname))
658 + if channel:
659 + if len(arguments) > 1:
660 + newtopic = arguments[1]
661 + channel.topic = newtopic
662 + self.message_channel(
663 + channel, "TOPIC", "%s :%s" % (channelname, newtopic),
664 + True)
665 + self.channel_log(
666 + channel, "set topic to %r" % newtopic, meta=True)
667 + else:
668 + if channel.topic:
669 + self.reply("332 %s %s :%s"
670 + % (self.nickname, channel.name,
671 + channel.topic))
672 + else:
673 + self.reply("331 %s %s :No topic is set"
674 + % (self.nickname, channel.name))
675 + else:
676 + self.reply("442 %s :You're not on that channel" % channelname)
677 +
678 + def wallops_handler():
679 + if len(arguments) < 1:
680 + self.reply_461(command)
681 + message = arguments[0]
682 + for client in server.clients.values():
683 + client.message(":%s NOTICE %s :Global notice: %s"
684 + % (self.prefix, client.nickname, message))
685 +
686 + def who_handler():
687 + if len(arguments) < 1:
688 + return
689 + targetname = arguments[0]
690 + if server.has_channel(targetname):
691 + channel = server.get_channel(targetname)
692 + for member in channel.members:
693 + self.reply("352 %s %s %s %s %s %s H :0 %s"
694 + % (self.nickname, targetname, member.user,
695 + member.host, server.name, member.nickname,
696 + member.realname))
697 + self.reply("315 %s %s :End of WHO list"
698 + % (self.nickname, targetname))
699 +
700 + def whois_handler():
701 + if len(arguments) < 1:
702 + return
703 + username = arguments[0]
704 + user = server.get_client(username)
705 + if user:
706 + self.reply("311 %s %s %s %s * :%s"
707 + % (self.nickname, user.nickname, user.user,
708 + user.host, user.realname))
709 + self.reply("312 %s %s %s :%s"
710 + % (self.nickname, user.nickname, server.name,
711 + server.name))
712 + self.reply("319 %s %s :%s"
713 + % (self.nickname, user.nickname,
714 + " ".join(user.channels)))
715 + self.reply("318 %s %s :End of WHOIS list"
716 + % (self.nickname, user.nickname))
717 + else:
718 + self.reply("401 %s %s :No such nick"
719 + % (self.nickname, username))
720 +
721 + handler_table = {
722 + "AWAY": away_handler,
723 + "ISON": ison_handler,
724 + "JOIN": join_handler,
725 + "LIST": list_handler,
726 + "LUSERS": lusers_handler,
727 + "MODE": mode_handler,
728 + "MOTD": motd_handler,
729 + "NICK": nick_handler,
730 + "NOTICE": notice_and_privmsg_handler,
731 + "PART": part_handler,
732 + "PING": ping_handler,
733 + "PONG": pong_handler,
734 + "PRIVMSG": notice_and_privmsg_handler,
735 + "QUIT": quit_handler,
736 + "TOPIC": topic_handler,
737 + "WALLOPS": wallops_handler,
738 + "WHO": who_handler,
739 + "WHOIS": whois_handler,
740 + }
741 + server = self.server
742 + valid_channel_re = self.__valid_channelname_regexp
743 + try:
744 + handler_table[command]()
745 + except KeyError:
746 + self.reply("421 %s %s :Unknown command" % (self.nickname, command))
747 +
748 + def udp_data_received(self, data):
749 + if data:
750 + message = self.infosec.unpack(data)
751 + if(message != None):
752 + self.message(message)
753 +
754 + def socket_readable_notification(self):
755 + try:
756 + data = self.socket.recv(2 ** 10)
757 + self.server.print_debug(
758 + "[%s:%d] -> %r" % (self.host, self.port, data))
759 + quitmsg = "EOT"
760 + except socket.error as x:
761 + data = ""
762 + quitmsg = x
763 + if data:
764 + self.__readbuffer += data
765 + self.__parse_read_buffer()
766 + self.__timestamp = time.time()
767 + self.__sent_ping = False
768 + for peer in self.server.peers:
769 + peer.send(self, data)
770 + else:
771 + self.disconnect(quitmsg)
772 +
773 + def socket_writable_notification(self):
774 + try:
775 + print("socket_writable_notification: %s" % self.__writebuffer)
776 + sent = self.socket.send(self.__writebuffer)
777 + self.server.print_debug(
778 + "[%s:%d] <- %r" % (
779 + self.host, self.port, self.__writebuffer[:sent]))
780 + self.__writebuffer = self.__writebuffer[sent:]
781 + except socket.error as x:
782 + self.disconnect(x)
783 +
784 + def disconnect(self, quitmsg):
785 + self.message("ERROR :%s" % quitmsg)
786 + self.server.print_info(
787 + "Disconnected connection from %s:%s (%s)." % (
788 + self.host, self.port, quitmsg))
789 + self.socket.close()
790 + self.server.remove_client(self, quitmsg)
791 +
792 + def message(self, msg):
793 + self.__writebuffer += msg + "\r\n"
794 +
795 + def reply(self, msg):
796 + self.message(":%s %s" % (self.server.name, msg))
797 +
798 + def reply_403(self, channel):
799 + self.reply("403 %s %s :No such channel" % (self.nickname, channel))
800 +
801 + def reply_461(self, command):
802 + nickname = self.nickname or "*"
803 + self.reply("461 %s %s :Not enough parameters" % (nickname, command))
804 +
805 + def message_channel(self, channel, command, message, include_self=False):
806 + line = ":%s %s %s" % (self.prefix, command, message)
807 + for client in channel.members:
808 + if client != self or include_self:
809 + client.message(line)
810 +
811 + def channel_log(self, channel, message, meta=False):
812 + if not self.server.logdir:
813 + return
814 + if meta:
815 + format = "[%s] * %s %s\n"
816 + else:
817 + format = "[%s] <%s> %s\n"
818 + timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
819 + logname = channel.name.replace("_", "__").replace("/", "_")
820 + fp = open("%s/%s.log" % (self.server.logdir, logname), "a")
821 + fp.write(format % (timestamp, self.nickname, message))
822 + fp.close()
823 +
824 + def message_related(self, msg, include_self=False):
825 + clients = set()
826 + if include_self:
827 + clients.add(self)
828 + for channel in self.channels.values():
829 + clients |= channel.members
830 + if not include_self:
831 + clients.discard(self)
832 + for client in clients:
833 + client.message(msg)
834 +
835 + def send_lusers(self):
836 + self.reply("251 %s :There are %d users and 0 services on 1 server"
837 + % (self.nickname, len(self.server.clients)))
838 +
839 + def send_motd(self):
840 + server = self.server
841 + motdlines = server.get_motd_lines()
842 + if motdlines:
843 + self.reply("375 %s :- %s Message of the day -"
844 + % (self.nickname, server.name))
845 + for line in motdlines:
846 + self.reply("372 %s :- %s" % (self.nickname, line.rstrip()))
847 + self.reply("376 %s :End of /MOTD command" % self.nickname)
848 + else:
849 + self.reply("422 %s :MOTD File is missing" % self.nickname)
850 diff --git a/lib/funcs.py b/lib/funcs.py
851 new file mode 100644
852 index 0000000..4093964
853 --- /dev/null
854 +++ b/lib/funcs.py
855 @@ -0,0 +1,11 @@
856 +import sys
857 +import string
858 +
859 +_maketrans = str.maketrans if sys.version_info[0] == 3 else string.maketrans
860 +_ircstring_translation = _maketrans(
861 + string.ascii_lowercase.upper() + "[]\\^",
862 + string.ascii_lowercase + "{}|~")
863 +
864 +def irc_lower(s):
865 + return string.translate(s, _ircstring_translation)
866 +
867 diff --git a/lib/infosec.py b/lib/infosec.py
868 new file mode 100644
869 index 0000000..6e87ca6
870 --- /dev/null
871 +++ b/lib/infosec.py
872 @@ -0,0 +1,29 @@
873 +import hashlib
874 +PACKET_SIZE = 1024
875 +MAX_MESSAGE_SIZE = 512
876 +
877 +class Infosec(object):
878 + #def __init__(self):
879 + # do nothing
880 +
881 + def pack(self, message):
882 + digest = hashlib.sha512(self._pad(message)).hexdigest()
883 + return digest + message
884 +
885 + def unpack(self, package):
886 + print("received package: %s" % package)
887 + received_digest = package[0:128]
888 + message = package[128:1023]
889 + digest = hashlib.sha512(self._pad(message)).hexdigest()
890 + print("received_digest: %s" % received_digest)
891 + print("digest: %s" % digest)
892 + print("message: %s") % message
893 + if(received_digest == digest):
894 + return message
895 + else:
896 + print("unable to validate package: %s" % package)
897 + return None
898 +
899 + def _pad(self, text):
900 + return str(text.ljust(MAX_MESSAGE_SIZE)).encode("ascii")
901 +
902 diff --git a/lib/peer.py b/lib/peer.py
903 new file mode 100644
904 index 0000000..4a64ed7
905 --- /dev/null
906 +++ b/lib/peer.py
907 @@ -0,0 +1,13 @@
908 +import socket
909 +from infosec import Infosec
910 +
911 +class Peer(object):
912 + def __init__(self, address):
913 + self.address = address
914 + self.socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
915 + self.infosec = Infosec()
916 +
917 + def send(self, client, msg):
918 + full_message = str.encode(":%s %s" % (client.nickname, msg))
919 + print("sending formatted_msg: %s" % full_message)
920 + self.socket.sendto(self.infosec.pack(full_message), (self.address, 7778))
921 diff --git a/lib/server.py b/lib/server.py
922 new file mode 100644
923 index 0000000..38bf514
924 --- /dev/null
925 +++ b/lib/server.py
926 @@ -0,0 +1,208 @@
927 +VERSION = "9999"
928 +
929 +import os
930 +import select
931 +import socket
932 +import sys
933 +import sys
934 +import tempfile
935 +import time
936 +import string
937 +from datetime import datetime
938 +from lib.client import Client
939 +from lib.channel import Channel
940 +from lib.infosec import PACKET_SIZE
941 +from lib.infosec import Infosec
942 +from lib.peer import Peer
943 +from funcs import *
944 +
945 +class Server(object):
946 + def __init__(self, options):
947 + self.ports = options.ports
948 + self.peers = options.peers
949 + self.udp_port = options.udp_port
950 + self.password = options.password
951 + self.ssl_pem_file = options.ssl_pem_file
952 + self.motdfile = options.motd
953 + self.verbose = options.verbose
954 + self.debug = options.debug
955 + self.logdir = options.logdir
956 + self.chroot = options.chroot
957 + self.setuid = options.setuid
958 + self.statedir = options.statedir
959 +
960 + if options.listen:
961 + self.address = socket.gethostbyname(options.listen)
962 + else:
963 + self.address = ""
964 + server_name_limit = 63 # From the RFC.
965 + self.name = socket.getfqdn(self.address)[:server_name_limit]
966 +
967 + self.channels = {} # irc_lower(Channel name) --> Channel instance.
968 + self.clients = {} # Socket --> Client instance..peers = ""
969 + self.nicknames = {} # irc_lower(Nickname) --> Client instance.
970 + if self.logdir:
971 + create_directory(self.logdir)
972 + if self.statedir:
973 + create_directory(self.statedir)
974 +
975 + def daemonize(self):
976 + try:
977 + pid = os.fork()
978 + if pid > 0:
979 + sys.exit(0)
980 + except OSError:
981 + sys.exit(1)
982 + os.setsid()
983 + try:
984 + pid = os.fork()
985 + if pid > 0:
986 + self.print_info("PID: %d" % pid)
987 + sys.exit(0)
988 + except OSError:
989 + sys.exit(1)
990 + os.chdir("/")
991 + os.umask(0)
992 + dev_null = open("/dev/null", "r+")
993 + os.dup2(dev_null.fileno(), sys.stdout.fileno())
994 + os.dup2(dev_null.fileno(), sys.stderr.fileno())
995 + os.dup2(dev_null.fileno(), sys.stdin.fileno())
996 +
997 + def get_client(self, nickname):
998 + return self.nicknames.get(irc_lower(nickname))
999 +
1000 + def has_channel(self, name):
1001 + return irc_lower(name) in self.channels
1002 +
1003 + def get_channel(self, channelname):
1004 + if irc_lower(channelname) in self.channels:
1005 + channel = self.channels[irc_lower(channelname)]
1006 + else:
1007 + channel = Channel(self, channelname)
1008 + self.channels[irc_lower(channelname)] = channel
1009 + return channel
1010 +
1011 + def get_motd_lines(self):
1012 + if self.motdfile:
1013 + try:
1014 + return open(self.motdfile).readlines()
1015 + except IOError:
1016 + return ["Could not read MOTD file %r." % self.motdfile]
1017 + else:
1018 + return []
1019 +
1020 + def print_info(self, msg):
1021 + if self.verbose:
1022 + print(msg)
1023 + sys.stdout.flush()
1024 +
1025 + def print_debug(self, msg):
1026 + if self.debug:
1027 + print(msg)
1028 + sys.stdout.flush()
1029 +
1030 + def print_error(self, msg):
1031 + sys.stderr.write("%s\n" % msg)
1032 +
1033 + def client_changed_nickname(self, client, oldnickname):
1034 + if oldnickname:
1035 + del self.nicknames[irc_lower(oldnickname)]
1036 + self.nicknames[irc_lower(client.nickname)] = client
1037 +
1038 + def remove_member_from_channel(self, client, channelname):
1039 + if irc_lower(channelname) in self.channels:
1040 + channel = self.channels[irc_lower(channelname)]
1041 + channel.remove_client(client)
1042 +
1043 + def remove_client(self, client, quitmsg):
1044 + client.message_related(":%s QUIT :%s" % (client.prefix, quitmsg))
1045 + for x in client.channels.values():
1046 + client.channel_log(x, "quit (%s)" % quitmsg, meta=True)
1047 + x.remove_client(client)
1048 + if client.nickname \
1049 + and irc_lower(client.nickname) in self.nicknames:
1050 + del self.nicknames[irc_lower(client.nickname)]
1051 + del self.clients[client.socket]
1052 +
1053 + def remove_channel(self, channel):
1054 + del self.channels[irc_lower(channel.name)]
1055 +
1056 + def start(self):
1057 + # Setup UDP first
1058 + udp_server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
1059 + udp_server_socket.bind((self.address, self.udp_port))
1060 +
1061 + serversockets = []
1062 + for port in self.ports:
1063 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1064 + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1065 + try:
1066 + s.bind((self.address, port))
1067 + except socket.error as e:
1068 + self.print_error("Could not bind port %s: %s." % (port, e))
1069 + sys.exit(1)
1070 + s.listen(5)
1071 + serversockets.append(s)
1072 + del s
1073 + self.print_info("Listening on port %d." % port)
1074 + if self.chroot:
1075 + os.chdir(self.chroot)
1076 + os.chroot(self.chroot)
1077 + self.print_info("Changed root directory to %s" % self.chroot)
1078 + if self.setuid:
1079 + os.setgid(self.setuid[1])
1080 + os.setuid(self.setuid[0])
1081 + self.print_info("Setting uid:gid to %s:%s"
1082 + % (self.setuid[0], self.setuid[1]))
1083 + last_aliveness_check = time.time()
1084 + while True:
1085 + (inputready,outputready,exceptready) = select.select([udp_server_socket],[],[],0)
1086 + (iwtd, owtd, ewtd) = select.select(
1087 + serversockets + [x.socket for x in self.clients.values()],
1088 + [x.socket for x in self.clients.values()
1089 + if x.write_queue_size() > 0],
1090 + [],
1091 + 0)
1092 + for x in inputready:
1093 + if x == udp_server_socket:
1094 + bytes_address_pair = udp_server_socket.recvfrom(PACKET_SIZE)
1095 + message = bytes_address_pair[0]
1096 + address = bytes_address_pair[1]
1097 + print message
1098 + for c in self.clients:
1099 + self.clients[c].udp_data_received(message)
1100 + for x in iwtd:
1101 + if x in self.clients:
1102 + self.clients[x].socket_readable_notification()
1103 + else:
1104 + (conn, addr) = x.accept()
1105 + if self.ssl_pem_file:
1106 + import ssl
1107 + try:
1108 + conn = ssl.wrap_socket(
1109 + conn,
1110 + server_side=True,
1111 + certfile=self.ssl_pem_file,
1112 + keyfile=self.ssl_pem_file)
1113 + except ssl.SSLError as e:
1114 + self.print_error(
1115 + "SSL error for connection from %s:%s: %s" % (
1116 + addr[0], addr[1], e))
1117 + continue
1118 + self.clients[conn] = Client(self, conn)
1119 + self.print_info("Accepted connection from %s:%s." % (
1120 + addr[0], addr[1]))
1121 + for x in owtd:
1122 + if x in self.clients: # client may have been disconnected
1123 + self.clients[x].socket_writable_notification()
1124 + now = time.time()
1125 + if last_aliveness_check + 10 < now:
1126 + for client in self.clients.values():
1127 + client.check_aliveness()
1128 + last_aliveness_check = now
1129 +
1130 +
1131 +def create_directory(path):
1132 + if not os.path.isdir(path):
1133 + os.makedirs(path)
1134 +
1135

Leave a Reply