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
1 | diff --git a/Makefile b/Makefile |
2 | new file mode 100644 |
3 | index 0000000..1db3ff4 |
4 | |
5 | |
6 | |
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/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g; s/'"'"'/\'/g; s/((/\(\(/g; s/))/\)\)/g; s/\[([0-9])\[/\[$1\[/g; s/]]/\]\]/g' > genesis.vdiff.escaped |
30 | diff --git a/README.txt b/README.txt |
31 | new file mode 100644 |
32 | index 0000000..2d5ff88 |
33 | |
34 | |
35 | |
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 | |
65 | |
66 | |
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 | |
217 | |
218 | |
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 | |
227 | |
228 | |
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 | |
234 | |
235 | |
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 | |
300 | |
301 | |
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 | |
854 | |
855 | |
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 | |
871 | |
872 | |
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 | |
906 | |
907 | |
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 | |
925 | |
926 | |
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 |