diff --git a/.scripts/ccproxy.py b/.scripts/ccproxy.py new file mode 100755 index 0000000..e9ccbd6 --- /dev/null +++ b/.scripts/ccproxy.py @@ -0,0 +1,186 @@ +# +# Comic Chat fixer MITM proxy: fixes Comic Chat to (sort of) work with modern +# IRC servers. Tested with Microsoft Chat 2.5 on Windows XP, 8 and 10 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# + +import getopt, re, socket, sys, threading, time +try: + import ssl +except ImportError: + ssl = None + +def thread_c2s(client, client_addr, password, host, port, use_ssl): + f = client.makefile() + + queued_lines = [] + if password: + client.sendall(':cchat.proxy 464 * :Password incorrect\r\n') + while True: + line = f.readline().rstrip('\r\n') + if line[:12] == 'OPER (null) ': + if password == line[12:]: + print '[-] {0}:{1} authenticated successfully'.format(*client_addr) + break + else: + print '[!] {0}:{1} failed to authenticate'.format(*client_addr) + client.sendall(':cchat.proxy 464 * :Password incorrect\r\n') + else: + queued_lines.append(line) + + irc = socket.create_connection((host, port)) + if use_ssl: + if not ssl: raise Exception('no ssl module') + irc = ssl.wrap_socket(irc) + + for line in queued_lines: + irc.sendall(line + '\r\n') + + t = threading.Thread(target=thread_s2c, args=(client, client_addr, irc)) + t.daemon = True + t.start() + + try: + while True: + line = f.readline() + irc.sendall(line) + if len(line) == 0 or line[:5] == 'QUIT ': break + except KeyboardInterrupt: + sys.exit(1) + except: + pass + + try: + irc.close() + except: + pass + try: + client.close() + except: + pass + +def thread_s2c(client, client_addr, irc): + f = irc.makefile() + + srv_prefix = '@+' + + try: + while True: + line = f.readline() + + split = line.split(' ') + if len(split) > 2: + if split[0] == 'ERROR': + client.sendall(line) + break + elif split[1] == '005': + # Get PREFIX= to fix ranks in the NAMES response + match = re.search(''' PREFIX=\(([^\)]+)\)([^\s]+)''', line) + if match: + srv_prefix = match.group(2) + elif split[1] == 'JOIN' and split[2][0] != ':': + # Main purpose of the proxy. Fixes a crash bug with newer + # ircds, which send JOIN confirmations like this: + # + # :nick!user@host JOIN #channel + # + # instead of this: + # + # :nick!user@host JOIN :#channel + # + # CChat expects the channel name to have a : before the + # name. If it doesn't, it will crash, since it somehow + # attempts a stricmp(0). + split[2] = ':' + split[2] + elif split[1] == '353': + # Convert additional ranks to regular op + for i in range(5, len(split)): + rank = '' + nick = '' + for char in split[i]: + if char == '+' and rank != '@': + # voice + rank = '+' + elif char in srv_prefix: + # everything unknown to CChat becomes op + rank = '@' + elif char != ':': + # not a rank + nick += char + split[i] = (split[i][0] == ':' and ':' or '') + rank + nick + line = ' '.join(split) + + # Comic Chat will stop receiving if it receives a line longer than + # 512 bytes, including the trailing CRLF. + client.sendall(line.rstrip('\r\n')[:510] + '\r\n') + except KeyboardInterrupt: + sys.exit(1) + + try: + irc.close() + except: + pass + try: + client.close() + except: + pass + +def main(): + bind_host = '' + bind_port = 6461 + password = None + + options, remainder = getopt.getopt(sys.argv[1:], 'h:p:a:', ['bindhost=', 'bindport=', 'password=']) + for opt, arg in options: + if opt in ('-h', '--bindhost'): + bind_host = arg + elif opt in ('-p', '--bindport'): + bind_port = int(arg) + elif opt in ('-a', '--password'): + password = arg + + if bind_port < 0 or bind_port > 65535 or len(remainder) < 1: + print 'Usage: proxy.py [-h bindhost] [-p bindport] [-a password] server [[+]port]' + sys.exit(1) + + host = remainder[0] + if len(remainder) > 1: + if remainder[1][0] == '+': + use_ssl = True + port = int(remainder[1][1:]) + else: + use_ssl = False + port = int(remainder[1]) + else: + use_ssl = False + port = 6667 + + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((bind_host, bind_port)) + server.listen(5) + + print '[-] Waiting for connections' + + try: + while True: + client, client_addr = server.accept() + print '[-] Connection from {0}:{1}'.format(*client_addr) + t = threading.Thread(target=thread_c2s, args=(client, client_addr, password, host, port, use_ssl)) + t.daemon = True + t.start() + except KeyboardInterrupt: + server.close() + sys.exit(1) + +if __name__ == '__main__': + main()