From 3882fe2ec9f5e39d4e50031bf34b5813e8a1297a Mon Sep 17 00:00:00 2001 From: emdee Date: Sun, 23 Oct 2022 22:11:12 +0000 Subject: [PATCH 01/10] First refactor --- README.md | 26 ++- tox-irc-sync.py | 547 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 481 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index de88c36..8cdaa6b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ #Tox-Sync -A bot that sync messages between Freenode IRC #tox-ontopic and Tox group chat. +A bot that sync messages between IRC and Tox group chat. + +## Hard forked + +Hard forked to use https://git.macaw.me/emdee/toxygen_wrapper +Just clone that repo and put the resulting directory on your +```PYTHONPATH```. + +Run: ```tox-irc-sync.py --help`` for command line arguments. + +For the ```#tox``` group on ```libera.chat```: +For example```irc.libera.net#tox```: +``` +python3 tox-irc-sync.py \ + --nodes_json $HOME/.config/tox/DHTnodes.json \ + --irc_chan "#tor" --irc_host irc.libera.net --irc_port 6667 \ +``` + +Libera will not work over Tor, but ```irc.oftc.net#tor``` will: +``` +python3 tox-irc-sync.py \ + --nodes_json $HOME/.config/tox/DHTnodes.json \ + --irc_chan "#tor" --irc_host irc.oftc.net --irc_port 6667 \ + --proxy_type 2 --proxy_host 127.0.0.1 --proxy_port 9050 +``` diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 9c386ec..1102552 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -1,43 +1,56 @@ import sys +import os import socket import string import select import re import pickle - -from pytox import Tox, ToxAV +import logging +import readline +import ctypes from time import sleep from os.path import exists from threading import Thread +from random import shuffle + +import wrapper +from wrapper.tox import Tox +from wrapper.toxav import ToxAV +import wrapper.toxcore_enums_and_consts as enums +from wrapper.toxcore_enums_and_consts import \ + TOX_CONNECTION, TOX_USER_STATUS, TOX_MESSAGE_TYPE, \ + TOX_SECRET_KEY_SIZE, TOX_FILE_CONTROL, TOX_ADDRESS_SIZE, \ + TOX_GROUP_PRIVACY_STATE, TOX_GROUP_ROLE + +try: + import support_testing as ts +except ImportError: + import wrapper_tests.support_testing as ts + +global LOG +LOG = logging.getLogger('app.'+'ts') -SERVER = ['54.199.139.199', 33445, '7F9C31FE850E97CEFD4C4591DF93FC757C7C12549DDD55F8EEAECC34FE76C029'] -GROUP_BOT = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5' PWD = '' -IRC_HOST = 'irc.freenode.net' -IRC_PORT = 6667 -NAME = NICK = IDENT = REALNAME = 'SyncBot' - -CHANNEL = '#tox-ontopic' -MEMORY_DB = 'memory.pickle' +NAME = NICK = IDENT = REALNAME = 'SyniTox' class AV(ToxAV): - def __init__(self, core, max_calls): - self.core = self.get_tox() + def __init__(self, core): + self.core = core self.cs = None - self.call_type = self.TypeAudio + self.call_type = None def on_invite(self, idx): self.cs = self.get_peer_csettings(idx, 0) self.call_type = self.cs['call_type'] - print('Incoming %s call from %d:%s ...' % ( + LOG.info('Incoming %s call from %d:%s ...' % ( 'video' if self.call_type == self.TypeVideo else 'audio', idx, self.core.get_name(self.get_peer_id(idx, 0)))) self.answer(idx, self.call_type) - print('Answered, in call...') + LOG.info('Answered, in call...') def on_start(self, idx): self.change_settings(idx, {'max_video_width': 1920, @@ -48,7 +61,7 @@ class AV(ToxAV): def on_end(self, idx): self.kill_transmission() - print('Call ended') + LOG.info('Call ended') def on_peer_timeout(self, idx): self.stop_call() @@ -63,40 +76,195 @@ class AV(ToxAV): sys.stdout.flush() self.send_video(idx, width, height, data) -bot_toxname = 'SyncBot' +bot_toxname = 'SyniTox' -class SyncBot(Tox): - def __init__(self): - if exists('data'): - self.load_from_file('data') +class SyniTox(Tox): + + def __init__(self, opts, + sChannel='#tor', + sIRC_HOST='irc.oftc.net', + iIRC_PORT=6667, + GROUP_BOT_PK = '', + sMEMORY_DB = '' + ): + Tox.__init__(self, tox_options=opts) + self._address = self.self_get_address() + self._opts = opts + self._app = None + self._settings = {} + self._sChannel = sChannel + self.sIRC_HOST = sIRC_HOST + self.iIRC_PORT = iIRC_PORT + self.sGROUP_BOT_PK = GROUP_BOT_PK + self.sMEMORY_DB = sMEMORY_DB + + global oTOX_OARGS + self._oArgs = oTOX_OARGS + data = self._oArgs.profile + if data and os.path.exists(data): + self.load_from_file(data) - self.av = AV(self, 10) - self.connect() - self.set_name(bot_toxname) - self.set_status_message("Send me a message with the word 'invite'") - print('ID: %s' % self.get_address()) + self.av = self.AV + self.irc = None + self.bid = -1 + self._bRouted = None + + def start(self): + + self.self_set_name(bot_toxname) + self.self_set_status_message("Send me a message with the word 'invite'") + LOG.info('Our ToxID: %s' % self.self_get_toxid()) - self.readbuffer = '' + self.readbuffer = b'' + self.tox_group_id = None - - self.irc_init() + self.group_init() + self.memory = {} - - if exists(MEMORY_DB): - with open(MEMORY_DB, 'r') as f: + if os.path.exists(self.sMEMORY_DB): + with open(self.sMEMORY_DB, 'r') as f: self.memory = pickle.load(f) + + self.irc_init() + b = self.test_net() + if b: + self.dht_init() + + def bRouted(self): + if self._oArgs.network not in ['local', 'localnew', 'newlocal']: + b = ts.bAreWeConnected() + if b is None: + i = os.system('ip route|grep ^def') + if i > 0: + b = False + else: + b = True + if not b: + LOG.warn("No default route for network " +self._oArgs.network) + return False + return b + return True + + def test_net(self, lElts=None, oThread=None, iMax=4): + # bootstrap + lNodes = ts.generate_nodes(oArgs=self._oArgs, + ipv='ipv4', + udp_not_tcp=True) + self._settings['current_nodes_udp'] = lNodes.copy() + if not lNodes: + LOG.warn('empty generate_nodes udp') + else: + LOG.debug(f'Called generate_nodes: udp {len(lNodes)}') + + lNodes = ts.generate_nodes(oArgs=self._oArgs, + ipv='ipv4', + udp_not_tcp=False) + self._settings['current_nodes_tcp'] = lNodes + if not lNodes: + LOG.warn('empty generate_nodes tcp') + else: + LOG.debug(f'Called generate_nodes: tcp {len(lNodes)}') + + # if oThread and oThread._stop_thread: return + LOG.debug("test_net network=" +self._oArgs.network +' iMax=' +str(iMax)) + return True + + def group_init(self): + LOG.debug(f"group_init proxy={self._oArgs.proxy_type}") + group_name = bot_toxname +' Test ' +self._sChannel + if not self.sGROUP_BOT_PK: + privacy_state = TOX_GROUP_PRIVACY_STATE['PUBLIC'] + nick = bot_toxname +self._sChannel + status = TOX_USER_STATUS['NONE'] + num = self.group_new(privacy_state, group_name, nick, status) + assert num >= 0, num + + pk = self.group_self_get_public_key(num) + assert pk, pk + self.sGROUP_BOT_PK = pk + self.sGROUP_NUM = num + + self.group_set_topic(num, bot_toxname +" IRC") + LOG.info(f"group_init GROUP_BOT_PK={self.sGROUP_BOT_PK}") + #? self.tox_group_id = self.bid + self.group_send_message(num, TOX_MESSAGE_TYPE['NORMAL'], "hi") + # TOX_GROUP_ROLE['FOUNDER'] + self.init_callbacks() + + def init_callbacks(self): + def gi_wrapped(iTox, friendid, invite_data, invite_len, *args): + invite_data = str(invite_data, 'UTF-8') + self.on_group_invite(friendid, invite_data) + self.callback_group_invite(gi_wrapped, 0) + def scs_wrapped(iTox, friendid, *args): + self.on_connection_status(self, scs_wrapped) + self.callback_self_connection_status(scs_wrapped) + def gm_wrapped(iTox, groupnumber, peer_id, type_, message, mlen, *args): + message = str(message, 'UTF-8') + self.on_group_message(groupnumber, peer_id, message) + self.callback_group_message(gm_wrapped, 0) + def ga_wrapped(iTox, groupnumber, peer_id, type_, action, mlen, *args): + self.on_group_action(groupnumber, peer_id, action) + #? self.callback_group_action(ga_wrapped, 0) + def fr_wrapped(iTox, pk, message, mlen, *args): + message = str(message, 'UTF-8') + self.on_friend_request(self, pk, message) + self.callback_friend_request(fr_wrapped) + def fm_wrapped(iTox, peer_id, message, mlen, *args): + message = str(message, 'UTF-8') + self.on_friend_request(self, peer_id, message) + self.callback_friend_request(fm_wrapped) + + def del_callbacks(self): + self.callback_group_invite(None, 0) + self.callback_self_connection_status(None) + self.callback_group_message(None, 0) + # self.callback_group_action(None, 0) + self.callback_friend_request(None) + self.callback_friend_request(None) def irc_init(self): - self.irc = socket.socket() - self.irc.connect((IRC_HOST, IRC_PORT)) - self.irc.send('NICK %s\r\n' % NICK) - self.irc.send('USER %s %s bla :%s\r\n' % (IDENT, IRC_HOST, REALNAME)) + if not self.bRouted(): return + + LOG.info(f"irc_init proxy={self._oArgs.proxy_type}") + if self._oArgs.proxy_type == 2: + from wrapper_tests import socks + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, + self._oArgs.proxy_host, + self._oArgs.proxy_port) + irc = socks.socksocket() + else: + irc = socket.socket() + try: + irc.connect((self.sIRC_HOST, self.iIRC_PORT)) + irc.send(bytes('NICK ' + NICK + '\r\n', 'UTF-8' )) + irc.send(bytes('USER %s %s bla :%s\r\n' % (IDENT, self.sIRC_HOST, REALNAME), + 'UTF-8')) + except Exception as e: + LOG.warn(f'IRC error {e}') + else: + LOG.info('IRC connected ' +'NICK =' + NICK) + self.irc = irc - def connect(self): - print('connecting...') - self.bootstrap_from_address(SERVER[0], SERVER[1], SERVER[2]) + def dht_init(self): + if not self.bRouted(): return + if 'current_nodes_udp' not in self._settings: + self.test_net() + lNodes = self._settings['current_nodes_udp'] + shuffle(lNodes) + if self._oArgs.proxy_type == 0: + ts.bootstrap_good(lNodes[:4], [self]) + else: + if self._bRouted == None: + LOG.info(f'DHT bootstapping 1') + ts.bootstrap_good([lNodes[0]], [self]) + if 'current_nodes_tcp' not in self._settings: + self.test_net() + lNodes = self._settings['current_nodes_tcp'] + shuffle(lNodes) + ts.bootstrap_tcp(lNodes[:4], [self]) - def ensure_exe(self, func, args): + def ensure_exe(self, func, *args): count = 0 THRESHOLD = 50 @@ -106,42 +274,115 @@ class SyncBot(Tox): except: assert count < THRESHOLD count += 1 - for i in range(10): - self.do() - sleep(0.02) + self.do() - def loop(self): + def do(self, n=50): + interval = self.iteration_interval() + for i in range(n): + self.iterate() + sleep(interval / 1000.0 *10) + + def unroute(self): + if self.irc: + try: irc.close() + except: pass + self.irc = None + + def irc_check(self, lines): + if b'NOTICE AUTH' in lines[0]: + for line in lines[:99]: + if b'NOTICE AUTH' not in line: return + line = str(line, 'UTF-8').strip() + print(line) + else: + for line in lines[:5]: + line = str(line, 'UTF-8').strip().lower() + if 'banned' in line: + raise RuntimeError(line) + if 'error' in line and 'closing' in line: + raise RuntimeError(line) + + def iLoop(self): checked = False self.joined = False self.request = False + count = 0 try: + count = count + 1 while True: - status = self.isconnected() - if not checked and status: - print('Connected to DHT.') + b = self.bRouted() + if not b: + self.unroute() + checked = False + if self._bRouted is None or self._bRouted != b: + self._bRouted = b + if count % 6 == 1: + LOG.info(f'Not routed {count}') + sleep(10) + continue + else: + if self._bRouted is None: + self._bRouted = True + self.irc_send('.') + if self._bRouted is None or self._bRouted != b: + self._bRouted = b + LOG.debug(f'Routed {count}') + + status = self.self_get_connection_status() + if not status: + if count % 6 == 1: + LOG.info(f'Not connected {count}') + self.dht_init() + + if b and not checked and status: + LOG.info('Connected to DHT.') checked = True try: - self.bid = self.get_friend_id(GROUP_BOT) - except: - self.ensure_exe(self.add_friend, (GROUP_BOT, 'Hi')) - self.bid = self.get_friend_id(GROUP_BOT) + self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK) + LOG.info(f'Connected to group {self.bid}') + except ctypes.ArgumentError as e: + self.bid = None + + if self.bid == None: + self.ensure_exe(self.friend_add_norequest, self.sGROUP_BOT_PK) + LOG.info(f'friend_add_n to group {self.sGROUP_BOT_PK[:8]}') + self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK) + LOG.info(f'Added to group {self.bid}') + num = self.sGROUP_NUM + my_pk = self.group_self_get_public_key(num) + LOG.info(f'Connected to group as {my_pk[:8]}') - if checked and not status: - print('Disconnected from DHT.') - self.connect() + if b and checked and not status: + LOG.info('Disconnected from DHT.') + self.dht_init() checked = False + + if not self.irc: + LOG.info('Disconnected from IRC.') + self.irc_init() + if not self.irc: + sleep(10) + continue + + LOG.info('Waiting on IRC.') + readable, _, _ = select.select([self.irc], [], [], 0.1) - readable, _, _ = select.select([self.irc], [], [], 0.01) - - if readable: + if not readable: + LOG.info('Waited on IRC but nothing to read.') + else: self.readbuffer += self.irc.recv(4096) - lines = self.readbuffer.split('\n') + lines = self.readbuffer.split(b'\n') + self.irc_check(lines) + LOG.info(f'Waited on IRC and got {len(lines)} lines.') self.readbuffer = lines.pop() - for line in lines: + line = str(line, 'UTF-8') + i = line.find(' ') + print(line[i+1:]) + l = line.rstrip().split() rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % - CHANNEL, line, re.S) + self._sChannel, line, re.S) if rx: print('IRC> %s: %s' % rx.groups()) msg = '[%s]: %s' % rx.groups() @@ -151,31 +392,55 @@ class SyncBot(Tox): action = '[%s]: %s' % (rx.group(1), rx.group(2)[8:-1]) self.ensure_exe(self.group_action_send, - (self.tox_group_id, action)) + self.tox_group_id, action) elif self.tox_group_id != None: self.ensure_exe(self.group_message_send, - (self.tox_group_id, msg)) + self.tox_group_id, msg) if content.startswith('^'): self.handle_command(content) - l = line.rstrip().split() - if l[0] == 'PING': + elif l[0] == 'PING': self.irc_send('PONG %s\r\n' % l[1]) - if l[1] == '376': - self.irc.send('PRIVMSG NickServ :IDENTIFY %s %s\r\n' - % (NICK, PWD)) - self.irc.send('JOIN %s\r\n' % CHANNEL) + elif l[1] == '376': + # :End of /MOTD command + self.irc.send(bytes('PRIVMSG NickServ :IDENTIFY %s %s\r\n' + % (NICK, PWD,), 'UTF-8')) + self.irc.send(bytes('JOIN %s\r\n' % self._sChannel, 'UTF-8')) + elif l[1] == '421': + # 421 SyniTox .PRIVMSG :Unknown command + pass + elif l[1] == '477': + #477 SyniTox #tor :Cannot join channel (Need to be identified and verified to join this channel, '/msg NickServ help' to learn how to register and verify.) + self.irc.send(bytes('HELP \r\n', 'UTF-8')) + self.irc.send(bytes('MSG NickServ help\r\n', 'UTF-8')) + + pass + self.do() except KeyboardInterrupt: - self.save_to_file('data') + ret = 0 + except Exception as e: + LOG.exception(f'Error running program:\n{e}', exc_info=True) + ret = 1 + else: + ret = 0 + self.quit() + return ret + + def quit(self): + self.del_callbacks() + self.save_to_file() + + def save_to_file(self): + pass def irc_send(self, msg): success = False while not success: try: - self.irc.send(msg) + self.irc.send(bytes(msg, 'UTF-8')) success = True break except socket.error: @@ -185,56 +450,57 @@ class SyncBot(Tox): def on_connection_status(self, friendId, status): if not self.request and not self.joined \ and friendId == self.bid and status: - print('Groupbot online, trying to join group chat.') + LOG.info('Groupbot online, trying to join group chat.') self.request = True - self.ensure_exe(self.send_message, (self.bid, 'invite')) + self.ensure_exe(self.send_message, self.bid, 'invite') - def on_group_invite(self, friendid, type, data): + def on_group_invite(self, friendid, invite_data, user_data): if not self.joined: self.joined = True self.tox_group_id = self.join_groupchat(friendid, data) - print('Joined groupchat.') + LOG.info('Joined groupchat.') - def on_group_message(self, groupnumber, friendgroupnumber, message): - name = self.group_peername(groupnumber, friendgroupnumber) + def on_group_message(self, groupnumber, peer_id, message): + name = self.group_peername(groupnumber, peer_id) if len(name) and name != NAME: print('TOX> %s: %s' % (name, message)) if message.startswith('>'): message = '\x0309%s\x03' % message - self.irc_send('PRIVMSG %s :[%s]: %s\r\n' % - (CHANNEL, name, message)) + self.irc_send(b'PRIVMSG %s :[%s]: %s\r\n' % + (self._sChannel, name, message)) if message.startswith('^'): self.handle_command(message) - def on_group_action(self, groupnumber, friendgroupnumber, action): - name = self.group_peername(groupnumber, friendgroupnumber) + def on_group_action(self, groupnumber, peer_id, action): + """old? message type action?""" + name = self.group_peername(groupnumber, peer_id) if len(name) and name != NAME: print('TOX> %s: %s' % (name, action)) if action.startswith('>'): action = '\x0309%s\x03' % action self.irc_send('PRIVMSG %s :\x01ACTION [%s]: %s\x01\r\n' % - (CHANNEL, name, action)) + (self._sChannel, name, action)) def on_friend_request(self, pk, message): - print('Friend request from %s: %s' % (pk, message)) + LOG.info('Friend request from %s: %s' % (pk, message)) self.add_friend_norequest(pk) - print('Accepted.') + LOG.info('Accepted.') def on_friend_message(self, friendid, message): if message == 'invite': if not self.tox_group_id is None: - print('Inviting %s' % self.get_name(friendid)) + LOG.info('Inviting %s' % self.get_name(friendid)) self.invite_friend(friendid, self.tox_group_id) return else: message = 'Waiting for GroupBot, please try again in 1 min.' - self.ensure_exe(self.send_message, (friendid, message)) + self.ensure_exe(self.send_message, friendid, message) def send_both(self, content): - self.ensure_exe(self.group_message_send, (self.tox_group_id, content)) - self.irc_send('PRIVMSG %s :%s\r\n' % (CHANNEL, content)) + self.ensure_exe(self.group_message_send, self.tox_group_id, content) + self.irc_send('PRIVMSG %s :%s\r\n' % (self._sChannel, content)) def handle_command(self, cmd): cmd = cmd[1:] @@ -247,12 +513,111 @@ class SyncBot(Tox): subject = args[0] desc = ' '.join(args[1:]) self.memory[subject] = desc - with open(MEMORY_DB, 'w') as f: - pickle.dump(self.memory, f) + if self.sMEMORY_DB: + with open(self.sMEMORY_DB, 'w') as f: + pickle.dump(self.memory, f) self.send_both('Remembering ^%s: %s' % (subject, desc)) elif self.memory.has_key(cmd): self.send_both(self.memory[cmd]) -t = SyncBot() -t.loop() +def iMain(oArgs): + assert oTOX_OPTIONS + assert oTOX_OARGS + + sChannel = oArgs.irc_chan + sIRC_HOST = oArgs.irc_host + iIRC_PORT = oArgs.irc_port + + o = SyniTox(oTOX_OPTIONS, sChannel, sIRC_HOST, iIRC_PORT) + o.start() + ret = o.iLoop() + return ret + +def oToxygenToxOptions(oArgs): + data = None + tox_options = wrapper.tox.Tox.options_new() + if oArgs.proxy_type: + tox_options.contents.proxy_type = int(oArgs.proxy_type) + tox_options.contents.proxy_host = bytes(oArgs.proxy_host, 'UTF-8') + tox_options.contents.proxy_port = int(oArgs.proxy_port) + tox_options.contents.udp_enabled = False + else: + tox_options.contents.udp_enabled = oArgs.udp_enabled + if not os.path.exists('/proc/sys/net/ipv6'): + oArgs.ipv6_enabled = False + + tox_options.contents.tcp_port = int(oArgs.tcp_port) + + # overrides + tox_options.contents.local_discovery_enabled = False + tox_options.contents.dht_announcements_enabled = True + tox_options.contents.hole_punching_enabled = False + tox_options.contents.experimental_thread_safety = False + # REQUIRED!! + if oArgs.ipv6_enabled and not os.path.exists('/proc/sys/net/ipv6'): + LOG.warn('Disabling IPV6 because /proc/sys/net/ipv6 does not exist' + repr(oArgs.ipv6_enabled)) + tox_options.contents.ipv6_enabled = False + else: + tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled) + + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + #? tox_options.contents.log_callback = LOG + if tox_options._options_pointer: + # LOG.debug("Adding logging to tox_options._options_pointer ") + ts.vAddLoggerCallback(tox_options, ts.on_log) + else: + LOG.warn("No tox_options._options_pointer " +repr(tox_options._options_pointer)) + + return tox_options + +def oArgparse(lArgv): + parser = ts.oMainArgparser() + parser.add_argument('profile', type=str, nargs='?', default=None, + help='Path to Tox profile') + # irc.libera.net #tox will not work over Tor + parser.add_argument('--irc_host', type=str, default='irc.oftc.net') + parser.add_argument('--irc_port', type=int, default=6667) + parser.add_argument('--irc_chan', type=str, default='#tor') + oArgs = parser.parse_args(lArgv) + + for key in ts.lBOOLEANS: + if key not in oArgs: continue + val = getattr(oArgs, key) + setattr(oArgs, key, bool(val)) + + if hasattr(oArgs, 'sleep'): + if oArgs.sleep == 'qt': + pass # broken or gevent.sleep(idle_period) + elif oArgs.sleep == 'gevent': + pass # broken or gevent.sleep(idle_period) + else: + oArgs.sleep = 'time' + + return oArgs + +def main(lArgs=None): + global oTOX_OARGS + + if lArgs is None: lArgs = [] + oArgs = oArgparse(lArgs) + oTOX_OARGS = oArgs + global oTOX_OPTIONS + oTOX_OPTIONS = oToxygenToxOptions(oArgs) + ts.vSetupLogging(oArgs) +# ts.setup_logging(oArgs) + + return iMain(oArgs) + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) + +# Ran 34 tests in 86.589s OK (skipped=12) From 0928ef551dd2b4392440c8c810d98eb3358bdf28 Mon Sep 17 00:00:00 2001 From: emdee Date: Mon, 24 Oct 2022 07:51:19 +0000 Subject: [PATCH 02/10] Added SSL --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8cdaa6b..173e42a 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ Hard forked to use https://git.macaw.me/emdee/toxygen_wrapper Just clone that repo and put the resulting directory on your ```PYTHONPATH```. -Run: ```tox-irc-sync.py --help`` for command line arguments. +## Usage + +Run: ```tox-irc-sync.py --help``` for command line arguments. For the ```#tox``` group on ```libera.chat```: -For example```irc.libera.net#tox```: ``` python3 tox-irc-sync.py \ --nodes_json $HOME/.config/tox/DHTnodes.json \ @@ -25,3 +26,12 @@ python3 tox-irc-sync.py \ --irc_chan "#tor" --irc_host irc.oftc.net --irc_port 6667 \ --proxy_type 2 --proxy_host 127.0.0.1 --proxy_port 9050 ``` + +* OFTC has an Onion address: + ```ircs://oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion:6697``` +* Libera has an Onion address: + ```libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion``` + +but ```tox_irc_sync``` does not do SSL yet. + + From 1e574520b064a33de23cbc78d3a738187def313f Mon Sep 17 00:00:00 2001 From: emdee Date: Wed, 26 Oct 2022 08:44:57 +0000 Subject: [PATCH 03/10] Extended groups code --- README.md | 133 ++++++- tox-irc-sync.py | 960 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 800 insertions(+), 293 deletions(-) diff --git a/README.md b/README.md index 173e42a..7798c63 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -#Tox-Sync +# Tox-Sync -A bot that sync messages between IRC and Tox group chat. +A bot that sync messages between IRC and Tox NGC group chat. -## Hard forked +## Hard Forked -Hard forked to use https://git.macaw.me/emdee/toxygen_wrapper +Hard forked from +and changed to use the Python wrapping from +. Just clone that repo and put the resulting directory on your ```PYTHONPATH```. @@ -12,11 +14,117 @@ Just clone that repo and put the resulting directory on your Run: ```tox-irc-sync.py --help``` for command line arguments. +``` +python3 tox-irc-sync.py \ + [-h] [--proxy_host PROXY_HOST] + [--proxy_port PROXY_PORT] + [--proxy_type {0,1,2}] + [--udp_enabled {True,False}] + [--ipv6_enabled {True,False}] + [--download_nodes_list {True,False}] + [--nodes_json NODES_JSON] + [--download_nodes_url DOWNLOAD_NODES_URL] + [--logfile LOGFILE] + [--loglevel LOGLEVEL] + [--tcp_port TCP_PORT] + [--mode MODE] + [--sleep {qt,gevent,time}] + [--irc_host IRC_HOST] + [--irc_port IRC_PORT] + [--irc_chan IRC_CHAN] + [--irc_ssl {,tls1.2,tls1.3}] + [--irc_ca IRC_CA] + [--irc_pem IRC_PEM] + [--irc_fp IRC_FP] + [--irc_nick IRC_NICK] + [--irc_name IRC_NAME] + [--irc_ident IRC_IDENT] + [--irc_pass IRC_PASS] + [--irc_email IRC_EMAIL] + [--group_pass GROUP_PASS] + [--group_name GROUP_NAME] + [--group_nick GROUP_NICK] + [--group_invite GROUP_INVITE] + [--group_moderator GROUP_MODERATOR] + [--group_ignore GROUP_IGNORE] + [profile] +``` + +### Positional arguments +``` + profile Path to Tox profile - new groups will be saved there +``` + +### Optional Arguments: + +``` + -h, --help show this help message and exit + + --proxy_host PROXY_HOST, --proxy-host PROXY_HOST + proxy host + --proxy_port PROXY_PORT, --proxy-port PROXY_PORT + proxy port + --proxy_type {0,1,2}, --proxy-type {0,1,2} + proxy type 1=http, 2=socks + + --udp_enabled {True,False} + En/Disable udp + --ipv6_enabled {False,False} + En/Disable ipv6 - default False + + --tcp_port TCP_PORT, --tcp-port TCP_PORT for serving as a Tox relay + + --mode MODE Mode: 0=chat 1=chat+audio 2=chat+audio+video default:0 + + --nodes_json NODES_JSON --network {old,main,local} + --download_nodes_url DOWNLOAD_NODES_URL + --download_nodes_list {True,False} + Download nodes list + + --logfile LOGFILE Filename for logging + --loglevel LOGLEVEL Threshold for logging (lower is more) default: 20 + + --irc_host IRC_HOST irc.libera.chat will not work over Tor + --irc_port IRC_PORT default 6667, but may be 6697 with SSL + --irc_chan IRC_CHAN IRC channel to join - include the # + + --irc_ssl {,tls1.2,tls1.3} TLS version; empty is no SSL + --irc_ca IRC_CA Certificate Authority file or directory + --irc_pem IRC_PEM Certificate and key as pem; use + openssl req -x509 -nodes -newkey rsa:2048 + --irc_fp IRC_FP fingerprint of the pem added with CERT ADD; use + openssl x509 -noout -fingerprint -SHA1 -text + --irc_nick IRC_NICK IRC Nickname + --irc_ident IRC_IDENT First field in USER + --irc_name IRC_NAME Third field in USER + --irc_pass IRC_PASS password for INDENTIFY or REGISTER + --irc_email IRC_EMAIL Use email to REGISTER with _pass + + --group_pass GROUP_PASS password for the group + --group_name GROUP_NAME name for the group + --group_nick GROUP_NICK Nickname of the group founder + --group_invite GROUP_INVITE A PK to invite to the group + --group_moderator GROUP_MODERATOR A PK to invite to the group as moderator + --group_ignore GROUP_IGNORE A PK to ignore by the group +``` + +### Examples + +The general idea here is to use this to create a profile, +and that profile will have one user, and one group to start with. +That profile will contain the founders keypair for the group, +so protect the profile as it contains the group's secret key. + +Then you use this profile to invite yourself to be a moderator, +by providing the your public key of the device you want to use to +moderate the group with the ```---group_moderator``` cmdline arg. + For the ```#tox``` group on ```libera.chat```: ``` python3 tox-irc-sync.py \ --nodes_json $HOME/.config/tox/DHTnodes.json \ - --irc_chan "#tor" --irc_host irc.libera.net --irc_port 6667 \ + --irc_chan "#tox" --irc_host irc.libera.net --irc_port 6667 \ + profile_that_will_get_the_group_key.tox ``` Libera will not work over Tor, but ```irc.oftc.net#tor``` will: @@ -24,7 +132,8 @@ Libera will not work over Tor, but ```irc.oftc.net#tor``` will: python3 tox-irc-sync.py \ --nodes_json $HOME/.config/tox/DHTnodes.json \ --irc_chan "#tor" --irc_host irc.oftc.net --irc_port 6667 \ - --proxy_type 2 --proxy_host 127.0.0.1 --proxy_port 9050 + --proxy_type 2 --proxy_host 127.0.0.1 --proxy_port 9050 \ + profile_that_will_get_the_group_key.tox ``` * OFTC has an Onion address: @@ -32,6 +141,16 @@ python3 tox-irc-sync.py \ * Libera has an Onion address: ```libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion``` -but ```tox_irc_sync``` does not do SSL yet. +## ChangeLog + +* changed to use the Python wrapping from +* ```tox_irc_sync``` does SSL now. + +### Future Directions + +1. It's intended as a IRC->Tox NGC gateway but it could work the other way round. +2. It could be a plugin under + which would broaden the range of callbacks that could be supported. +3. It could be a gateway to an existing NGC group with an invite bot. diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 1102552..9f1145b 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -1,18 +1,18 @@ +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + import sys import os import socket -import string import select import re import pickle import logging -import readline import ctypes from time import sleep -from os.path import exists from threading import Thread from random import shuffle +from OpenSSL import SSL import wrapper from wrapper.tox import Tox @@ -22,197 +22,385 @@ from wrapper.toxcore_enums_and_consts import \ TOX_CONNECTION, TOX_USER_STATUS, TOX_MESSAGE_TYPE, \ TOX_SECRET_KEY_SIZE, TOX_FILE_CONTROL, TOX_ADDRESS_SIZE, \ TOX_GROUP_PRIVACY_STATE, TOX_GROUP_ROLE +from wrapper_tests import socks try: import support_testing as ts except ImportError: import wrapper_tests.support_testing as ts +import wrapper.toxencryptsave as tox_encrypt_save + global LOG LOG = logging.getLogger('app.'+'ts') -PWD = '' -NAME = NICK = IDENT = REALNAME = 'SyniTox' - -class AV(ToxAV): - def __init__(self, core): - self.core = core - self.cs = None - self.call_type = None - - def on_invite(self, idx): - self.cs = self.get_peer_csettings(idx, 0) - self.call_type = self.cs['call_type'] - - LOG.info('Incoming %s call from %d:%s ...' % ( - 'video' if self.call_type == self.TypeVideo else 'audio', idx, - self.core.get_name(self.get_peer_id(idx, 0)))) - - self.answer(idx, self.call_type) - LOG.info('Answered, in call...') - - def on_start(self, idx): - self.change_settings(idx, {'max_video_width': 1920, - 'max_video_height': 1080}) - self.prepare_transmission(idx, self.jbufdc * 2, self.VADd, - True if self.call_type == self.TypeVideo else False) - - def on_end(self, idx): - self.kill_transmission() - - LOG.info('Call ended') - - def on_peer_timeout(self, idx): - self.stop_call() - - def on_audio_data(self, idx, size, data): - sys.stdout.write('.') - sys.stdout.flush() - self.send_audio(idx, size, data) - - def on_video_data(self, idx, width, height, data): - sys.stdout.write('*') - sys.stdout.flush() - self.send_video(idx, width, height, data) +NAME = 'SyniTox' +# possible CA locations picks the first one +lCAs = ['/etc/ssl/cacert.pem'] bot_toxname = 'SyniTox' +# tox.py can be called by callbacks +def LOG_ERROR(a): print('EROR> '+a) +def LOG_WARN(a): print('WARN> '+a) +def LOG_INFO(a): + bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 20 + if bVERBOSE: print('INFO> '+a) +def LOG_DEBUG(a): + bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel <= 10-1 + if bVERBOSE: print('DBUG> '+a) +def LOG_TRACE(a): + bVERBOSE = hasattr(__builtins__, 'app') and app.oArgs.loglevel < 10 + if bVERBOSE: print('TRAC> '+a) + +# https://wiki.python.org/moin/SSL +def ssl_verify_cb(HOST): + # wrapps host + def ssl_verify(*args): + """ + callback for certificate validation + should return true if verification passes and false otherwise + """ + LOG.debug(f"ssl_verify {len(args)} {args}") + ssl_conn, x509, error_num, depth, return_code = args + if error_num != 0: + return False + if depth != 0: + # don't validate names of root certificates + return True + + if x509.get_subject().commonName == HOST: + return True + + LOG.warn(f"ssl_verify {x509.get_subject().commonName} {HOST}") + # allow matching subdomains + have , want = x509.get_subject().commonName, HOST + if len(have.split('.')) == len(want.split('.')) and len(want.split('.')) > 2: + if have.split('.')[1:] == want.split('.')[1:]: + return True + + return False + + return ssl_verify + class SyniTox(Tox): - - def __init__(self, opts, - sChannel='#tor', - sIRC_HOST='irc.oftc.net', - iIRC_PORT=6667, + + def __init__(self, + oArgs, + oOpts, GROUP_BOT_PK = '', sMEMORY_DB = '' ): - Tox.__init__(self, tox_options=opts) - self._address = self.self_get_address() + + opts = oTOX_OPTIONS self._opts = opts + self._oArgs = oArgs + + # self._oArgs.profile + self.load_profile(self._opts, self._oArgs, self._oArgs.password) + Tox.__init__(self, tox_options=self._opts) + + self._address = self.self_get_address() self._app = None self._settings = {} - self._sChannel = sChannel - self.sIRC_HOST = sIRC_HOST - self.iIRC_PORT = iIRC_PORT - self.sGROUP_BOT_PK = GROUP_BOT_PK - self.sMEMORY_DB = sMEMORY_DB - global oTOX_OARGS - self._oArgs = oTOX_OARGS - data = self._oArgs.profile - if data and os.path.exists(data): - self.load_from_file(data) - self.av = self.AV self.irc = None self.bid = -1 self._bRouted = None + self._ssl_context = None + self._irc_id = '' + self._toxes = None + self.joined = None + self.request = None + self.memory = {} + self.readbuffer = b'' + #? tox_group_id + self._peers = [] + self._groups = {} + self.sMEMORY_DB = sMEMORY_DB + self.sGROUP_BOT_PK = GROUP_BOT_PK + self.sGROUP_BOT_NUM = -1 + + def load_profile(self, tox_options, oArgs, password=''): + if oArgs.profile and os.path.exists(oArgs.profile): + data = open(oArgs.profile, 'rb').read() + + else: + data = None + if data and self.has_password(): + data = self.pass_decrypt(data) + if data: # load existing profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] + tox_options.contents.savedata_data = ctypes.c_char_p(data) + tox_options.contents.savedata_length = len(data) + else: # create new profile + tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] + tox_options.contents.savedata_data = None + tox_options.contents.savedata_length = 0 + + def _save_profile(self, data=None): + LOG.debug("_save_profile") + data = data or self.get_savedata() + if self.has_password(): + data = self.pass_encrypt(data) + try: + suf = f"{os.getpid()}" + with open(self._oArgs.profile+suf, 'wb') as fl: + fl.write(data) + stat = os.stat(self._oArgs.profile+suf) + if hasattr(stat, 'st_blocks'): + assert stat.st_blocks > 0, f"Zero length file {self._oArgs.profile+suf}" + os.rename(self._oArgs.profile+suf, self._oArgs.profile) + LOG.info('Profile saved successfully to' +self._oArgs.profile) + except Exception as e: + LOG.warn(f"Profile save failed to {self._oArgs.profile}\n{e}") + def start(self): - - self.self_set_name(bot_toxname) + self._tox = self + self._toxes = tox_encrypt_save.ToxEncryptSave() + self.self_set_name(self._oArgs.bot_name) self.self_set_status_message("Send me a message with the word 'invite'") LOG.info('Our ToxID: %s' % self.self_get_toxid()) - self.readbuffer = b'' - self.tox_group_id = None - self.group_init() - - self.memory = {} + self.init_callbacks() + if os.path.exists(self.sMEMORY_DB): with open(self.sMEMORY_DB, 'r') as f: self.memory = pickle.load(f) - - self.irc_init() - b = self.test_net() - if b: - self.dht_init() + + if self._oArgs.irc_ssl != '': + self.start_ssl(self._oArgs.irc_host) + + def start_ssl(self, HOST): + if not self._ssl_context: + # TLSv1_3_METHOD does not exist + context = SSL.Context(SSL.TLSv1_2_METHOD) + context.set_options(SSL.OP_NO_SSLv2|SSL.OP_NO_SSLv3|SSL.OP_NO_TLSv1) + if self._oArgs.irc_pem: + val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT + LOG.info('Using keyfile: %s' % self._oArgs.irc_pem) + context.use_privatekey_file(self._oArgs.irc_pem) + else: + val = SSL.VERIFY_PEER + context.set_verify(val, ssl_verify_cb(self._oArgs.irc_host)) + + assert os.path.exists(self._oArgs.irc_ca), self._oArgs.irc_ca + if os.path.isdir(self._oArgs.irc_ca): + context.load_verify_locations(capath=self._oArgs.irc_ca) + else: + context.load_verify_locations(cafile=self._oArgs.irc_ca) + if self._oArgs.irc_ssl == 'tls1.2': + context.set_min_proto_version(SSL.TLS1_2_VERSION) + elif self._oArgs.irc_ssl == 'tls1.3': + context.set_min_proto_version(SSL.TLS1_3_VERSION) + self._ssl_context = context + + return self._ssl_context def bRouted(self): - if self._oArgs.network not in ['local', 'localnew', 'newlocal']: - b = ts.bAreWeConnected() - if b is None: - i = os.system('ip route|grep ^def') - if i > 0: - b = False - else: - b = True - if not b: - LOG.warn("No default route for network " +self._oArgs.network) - return False - return b - return True - + if self._oArgs.network in ['local']: + return True + b = ts.bAreWeConnected() + if b is None: + i = os.system('ip route|grep ^def') + if i > 0: + b = False + else: + b = True + self._bRouted = b + return b + def test_net(self, lElts=None, oThread=None, iMax=4): + LOG.debug("test_net network=" +self._oArgs.network ) # bootstrap lNodes = ts.generate_nodes(oArgs=self._oArgs, ipv='ipv4', udp_not_tcp=True) - self._settings['current_nodes_udp'] = lNodes.copy() + self._settings['current_nodes_udp'] = ts.sDNSClean(lNodes) if not lNodes: LOG.warn('empty generate_nodes udp') else: - LOG.debug(f'Called generate_nodes: udp {len(lNodes)}') + LOG.info(f'Called generate_nodes: udp {len(lNodes)}') lNodes = ts.generate_nodes(oArgs=self._oArgs, ipv='ipv4', udp_not_tcp=False) - self._settings['current_nodes_tcp'] = lNodes + self._settings['current_nodes_tcp'] = ts.sDNSClean(lNodes) if not lNodes: LOG.warn('empty generate_nodes tcp') else: - LOG.debug(f'Called generate_nodes: tcp {len(lNodes)}') - + LOG.info(f'Called generate_nodes: tcp {len(lNodes)}') + # if oThread and oThread._stop_thread: return - LOG.debug("test_net network=" +self._oArgs.network +' iMax=' +str(iMax)) return True - - def group_init(self): - LOG.debug(f"group_init proxy={self._oArgs.proxy_type}") - group_name = bot_toxname +' Test ' +self._sChannel - if not self.sGROUP_BOT_PK: - privacy_state = TOX_GROUP_PRIVACY_STATE['PUBLIC'] - nick = bot_toxname +self._sChannel - status = TOX_USER_STATUS['NONE'] - num = self.group_new(privacy_state, group_name, nick, status) - assert num >= 0, num - pk = self.group_self_get_public_key(num) - assert pk, pk - self.sGROUP_BOT_PK = pk - self.sGROUP_NUM = num + def add_friend(self, pk): + self.friend_add_norequest(pk) + assert self.friend_exists(pk) + assert pk in self.self_get_friend_list() + friend_number = self.friend_by_public_key(pk) + return friend_number - self.group_set_topic(num, bot_toxname +" IRC") - LOG.info(f"group_init GROUP_BOT_PK={self.sGROUP_BOT_PK}") - #? self.tox_group_id = self.bid + def start_groups(self): + if not self.bRouted(): return False + if not self.group_is_connected(self.sGROUP_BOT_NUM): + self.group_reconnect(self.sGROUP_BOT_NUM) + if not self.group_is_connected(self.sGROUP_BOT_NUM): + return False + assert self.sGROUP_BOT_NUM + num = self.sGROUP_BOT_NUM + self.group_self_set_status(num, TOX_USER_STATUS['NONE']) + + # add myself as a peer in the group or am I in as founder? self.group_send_message(num, TOX_MESSAGE_TYPE['NORMAL'], "hi") - # TOX_GROUP_ROLE['FOUNDER'] - self.init_callbacks() + # The code in tests_wrapper need extending and then + # wiring up to here. + # + if self._oArgs.group_invite: + pk = self._oArgs.group_invite + if pk not in self.self_get_friend_list(): + friend_number = self.add_friend(pk) + else: + friend_number = self.friend_by_public_key(pk) + b = self.group_invite_friend(num, friend_number) + LOG.info(f"A PK to invite to the group {b}") + return True + + if self._oArgs.group_moderator: + pk = self._oArgs.group_moderator + if pk not in self.self_get_friend_list(): + friend_number = self.add_friend(pk) + else: + friend_number = self.friend_by_public_key(pk) + role = TOX_GROUP_ROLE['MODERATOR'] + # dunno + peer_id = friend_number + b = self.group_mod_set_role(num, peer_id, role) + LOG.info("A PK to invite to the group as moderator {b}") + return True + + if self._oArgs.group_ignore: + pk = self._oArgs.group_ignore + if pk not in self.self_get_friend_list(): + friend_number = self.add_friend(pk) + else: + friend_number = self.friend_by_public_key(pk) + # dunno + peer_id = friend_number + b = self.group_toggle_set_ignore(num, peer_id, True) + LOG.info("A PK to ignore in the group {b}") + return True + + return None + + def create_group(self): + privacy_state = TOX_GROUP_PRIVACY_STATE[self._oArgs.group_state.upper()] + nick = self._oArgs.group_nick + group_name = self._oArgs.group_name + if not group_name: + group_name = self._oArgs.bot_name +self._oArgs.irc_chan + self._oArgs.group_name = group_name + status = TOX_USER_STATUS['NONE'] + num = self.group_new(privacy_state, group_name, nick, status) + assert num >= 0, num + self.group_set_topic(num, f"{group_name} IRC on {self._oArgs.irc_host}" ) + # self.tox_group_id = self.group_invite_accept(b'', friendid, nick) + + chat_id = self.group_get_chat_id(num) + if self._oArgs.profile and os.path.exists(os.path.dirname(self._oArgs.profile)): + f = os.path.splitext(self._oArgs.profile)[0] +'.chatid' + open(f, 'rt').write(chat_id) + LOG.info(f"Chat Id: {chat_id} written to {f}") + else: + LOG.info(f"Chat Id: {chat_id}") + # dunno + if self.self_get_friend_list(): + friendid = self.self_get_friend_list()[0] + i = on_group_invite(friendid, b'', 0) + assert i + self.tox_group_id = i + return num + + def join_group(self): + password = self._oArgs.group_pass + nick = self._oArgs.group_nick + # is the chat_id the pk? + chat_id = self._oArgs.group_chatid + num = self.group_join(chat_id, password, nick, status='') + self.sGROUP_BOT_NUM = num + self.group_self_set_status(num, TOX_USER_STATUS['NONE']) + return num + + def init_groups(self): + LOG.debug(f"init_groups proxy={self._oArgs.proxy_type}") + group_name = self._oArgs.bot_name +' Test ' +self._oArgs.irc_chan + if self.sGROUP_BOT_NUM < 0: + # ToDo: look for the first group of the profile + i = self.group_get_number_groups() + if i == 0: + if not self.bRouted(): return False + num = self.create_group() + self.sGROUP_BOT_NUM = num + elif i > 1: + LOG.error('There are more than one groups in this profile') + for ig in range(i): + LOG.warn(f"group #{ig} {self.group_self_get_name(ig)}") + raise RuntimeError("select one of the groups at the cmdline") + else: + if not self.bRouted(): return False + num = self.join_group() + + LOG.info(f"init_groups GROUP_BOT_PK={self.sGROUP_BOT_PK}") + + if self.bRouted(): + try: + self.start_groups() + except Exception as e: + LOG.warn(f"init_groups self.start_groups {e}") + return False + # TOX_GROUP_ROLE['FOUNDER'] + return True + def init_callbacks(self): + # wraps self with + LOG.info("Adding Tox init_callbacks") def gi_wrapped(iTox, friendid, invite_data, invite_len, *args): invite_data = str(invite_data, 'UTF-8') - self.on_group_invite(friendid, invite_data) + LOG.debug(f'on_group_invite {friendid} {invite_data}') + self.on_group_invite(friendid, invite_data, 0) self.callback_group_invite(gi_wrapped, 0) - def scs_wrapped(iTox, friendid, *args): - self.on_connection_status(self, scs_wrapped) + + def scs_wrapped(iTox, friendid, status, *args): + LOG.debug(f'on_connection_status {friendId} {status}.') + self.on_connection_status(friendid, status) self.callback_self_connection_status(scs_wrapped) + def gm_wrapped(iTox, groupnumber, peer_id, type_, message, mlen, *args): message = str(message, 'UTF-8') + LOG.debug(f'on_group_message {groupnumber} {peer_id} {message}') self.on_group_message(groupnumber, peer_id, message) self.callback_group_message(gm_wrapped, 0) + def ga_wrapped(iTox, groupnumber, peer_id, type_, action, mlen, *args): + LOG.debug(f'on_group_action(groupnumber, peer_id, action)') self.on_group_action(groupnumber, peer_id, action) + #? self.callback_group_action(ga_wrapped, 0) def fr_wrapped(iTox, pk, message, mlen, *args): message = str(message, 'UTF-8') - self.on_friend_request(self, pk, message) + LOG.debug(f'on_friend_request(pk, message)') + self.on_friend_request(pk, message) self.callback_friend_request(fr_wrapped) + def fm_wrapped(iTox, peer_id, message, mlen, *args): message = str(message, 'UTF-8') - self.on_friend_request(self, peer_id, message) + LOG.debug(f'on_friend_request(peer_id, message)') + self.on_friend_request(peer_id, message) self.callback_friend_request(fm_wrapped) def del_callbacks(self): @@ -225,26 +413,49 @@ class SyniTox(Tox): def irc_init(self): if not self.bRouted(): return - + nick = self._oArgs.irc_nick + realname = self._oArgs.irc_name + ident = self._oArgs.irc_ident + LOG.info(f"irc_init proxy={self._oArgs.proxy_type}") - if self._oArgs.proxy_type == 2: - from wrapper_tests import socks - socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, - self._oArgs.proxy_host, - self._oArgs.proxy_port) - irc = socks.socksocket() - else: - irc = socket.socket() try: - irc.connect((self.sIRC_HOST, self.iIRC_PORT)) - irc.send(bytes('NICK ' + NICK + '\r\n', 'UTF-8' )) - irc.send(bytes('USER %s %s bla :%s\r\n' % (IDENT, self.sIRC_HOST, REALNAME), - 'UTF-8')) + if self._oArgs.proxy_type == 2: + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, + self._oArgs.proxy_host, + self._oArgs.proxy_port) + irc = socks.socksocket() + elif self._oArgs.proxy_type == 1: + socks.setdefaultproxy(socks.PROXY_TYPE_HTTP, + self._oArgs.proxy_host, + self._oArgs.proxy_port) + irc = socks.socksocket() + else: + irc = socket.socket() + if self._oArgs.irc_ssl: + if not self._ssl_context: + self.start_ssl(self._oArgs.irc_host) + irc = SSL.Connection(self._ssl_context, irc) + irc.connect((self._oArgs.irc_host, self._oArgs.irc_port)) + irc.do_handshake() + LOG.info('IRC SSL connected ') + else: + irc.connect((self._oArgs.irc_host, self._oArgs.irc_port)) + LOG.info('IRC connected ') + + except ( SSL.Error, ) as e: + LOG.warn(f"SSL error: {e.args}") + return + except (SSL.SysCallError, ) as e: + LOG.warn(f"SSL error: {e.args}") + return except Exception as e: - LOG.warn(f'IRC error {e}') - else: - LOG.info('IRC connected ' +'NICK =' + NICK) - self.irc = irc + LOG.warn(f"Error: {e}") + return + + self.irc = irc + self.irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) + self.irc.send(bytes('USER %s %s bla :%s\r\n' % ( + ident, self._oArgs.irc_host, realname), 'UTF-8')) def dht_init(self): if not self.bRouted(): return @@ -253,21 +464,62 @@ class SyniTox(Tox): lNodes = self._settings['current_nodes_udp'] shuffle(lNodes) if self._oArgs.proxy_type == 0: - ts.bootstrap_good(lNodes[:4], [self]) + ts.bootstrap_udp(lNodes[:6], [self]) else: - if self._bRouted == None: - LOG.info(f'DHT bootstapping 1') - ts.bootstrap_good([lNodes[0]], [self]) + if self._bRouted is None: + LOG.info(f'UDP bootstapping 1') + ts.bootstrap_udp([lNodes[0]], [self]) if 'current_nodes_tcp' not in self._settings: self.test_net() lNodes = self._settings['current_nodes_tcp'] shuffle(lNodes) - ts.bootstrap_tcp(lNodes[:4], [self]) + LOG.info(f'TCP bootstapping 6') + ts.bootstrap_tcp(lNodes[:6], [self]) + + def get_all_groups(self): + try: + group_numbers = range(self._tox.group_get_number_groups()) + except Exception as e: + return None + groups = map(lambda n: self.get_group_by_number(n), group_numbers) + + return list(groups) + + def get_group_by_number(self, group_number): + try: + public_key = self._tox.group_get_chat_id(group_number) +# LOG.info(f"group_get_chat_id {group_number} {public_key}") + return self.get_group_by_public_key(public_key) + except Exception as e: + LOG.warn(f"group_get_chat_id {group_number} {e}") + return None + + def get_group_by_public_key(self, public_key, group): + self._groups[public_key] = group + + # ----------------------------------------------------------------------------------------------------------------- + # Group peers + # ----------------------------------------------------------------------------------------------------------------- + + def get_all_group_peers(self): + return list() + + def get_group_peer_by_public_key(self, group, public_key): + peer = group.get_peer_by_public_key(public_key) + + return self._get_group_peer(group, peer) + + def get_peer_by_id(self, peer_id): + peers = list(filter(lambda p: p.id == peer_id, self._peers)) + if peers: + return peers[0] + else: + LOG_WARN(f"get_peer_by_id empty peers for {peer_id}") + return [] def ensure_exe(self, func, *args): count = 0 THRESHOLD = 50 - while True: try: return func(*args) @@ -284,7 +536,7 @@ class SyniTox(Tox): def unroute(self): if self.irc: - try: irc.close() + try: self.irc.close() except: pass self.irc = None @@ -301,134 +553,187 @@ class SyniTox(Tox): raise RuntimeError(line) if 'error' in line and 'closing' in line: raise RuntimeError(line) - + + def irc_readlines(self): + nick = self._oArgs.irc_nick + pwd = self._oArgs.irc_pass + fp = self._oArgs.irc_fp + email = self._oArgs.irc_email + + self.readbuffer += self.irc.recv(4096) + lines = self.readbuffer.split(b'\n') + self.irc_check(lines) + LOG.debug(f'Waited on IRC and got {len(lines)} lines.') + self.readbuffer = lines.pop() + for line in lines: + line = str(line, 'UTF-8') + l = line.rstrip().split() + if len(l) < 2: + print(line) + elif l[1] not in ['372']: + i = line.find(' ') + print(line[i+1:]) + + rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % + self._oArgs.irc_chan, line, re.S) + if rx: + self.relay_message(rx) + elif l[0] == 'PING': + self.irc_send('PONG %s\r\n' % l[1]) + elif len(l) < 2: + pass + elif l[1] == '376': + # :End of /MOTD command + if email == '': + self.irc.send(bytes('PRIVMSG NickServ IDENTIFY %s %s\r\n' + % (nick, pwd,), 'UTF-8')) + else: + self.irc.send(bytes('PRIVMSG NickServ REGISTER %s %s\r\n' + % (pwd, email,), 'UTF-8')) + if False and fp: + LOG.info(f"PRIVMSG NickServ CERT ADD") +# self.irc.send(bytes(f'PRIVMSG NickServ CERT ADD {fp}\r\n', 'UTF-8')) + # + self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) + # put off init_groups until you have joined IRC + self.init_groups() + # Make sure we are in + + elif l[1] == '042': + # 042 SyniTox 8VQAADOD0 :your unique ID + self._irc_id = line.replace(' :your unique ID',''). \ + replace('042 '+nick +' ', '') + + elif l[1] == '421': + # 421 SyniTox .PRIVMSG :Unknown command + pass + elif l[1] == '477': + #477 SyniTox #tor :Cannot join channel (Need to be identified and verified to join this channel, '/msg NickServ help' to learn how to register and verify.) + LOG.info(f"PRIVMSG NickServ STATUS {nick}") + i = line.find("'/msg NickServ help'") + if i > 0: + line = line[:i] + raise RuntimeError(line) + + def relay_message(self, rx): + print('IRC> %s: %s' % rx.groups()) + msg = '[%s]: %s' % rx.groups() + content = rx.group(2) + + if self.sGROUP_BOT_NUM >= 0: + if content[1:].startswith('ACTION '): + action = '[%s]: %s' % (rx.group(1), + rx.group(2)[8:-1]) + type_ = TOX_MESSAGE_TYPE['ACTION'] + self.ensure_exe(self.group_send_message, + self.sGROUP_BOT_NUM, type_, action) + else: + type_ = TOX_MESSAGE_TYPE['NORMAL'] + self.ensure_exe(self.group_send_message, + self.sGROUP_BOT_NUM, type_, msg) + + if content.startswith('^'): + self.handle_command(content) + + def spin(self, n=20): + readable = False + waiti = 0 + while not readable: + waiti += 1 + readable, _, _ = select.select([self.irc], [], [], n/1000.0 ) + self.do(n) + if waiti > 100: break + return readable + def iLoop(self): - checked = False + group_connected = False + routed = None self.joined = False self.request = False - count = 0 - - try: - count = count + 1 + iCount = 0 + iDelay = 10 + + nick = self._oArgs.irc_nick + pwd = self._oArgs.irc_pass + email = self._oArgs.irc_email + LOG.info(f"Looping for Tox and IRC connections") + if iCount < self._oArgs.max_sleep: while True: + iCount += 1 +# LOG.debug(f"Looping {iCount}") b = self.bRouted() if not b: self.unroute() - checked = False - if self._bRouted is None or self._bRouted != b: - self._bRouted = b - if count % 6 == 1: - LOG.info(f'Not routed {count}') - sleep(10) + group_connected = False + iDelay = iDelay + iDelay // 10 + if routed != b: + if iCount % 10 == 1: + LOG.info(f'Not routed {iCount} sleeping {iDelay} seconds') + sleep(iDelay) continue - else: - if self._bRouted is None: - self._bRouted = True - self.irc_send('.') - if self._bRouted is None or self._bRouted != b: - self._bRouted = b - LOG.debug(f'Routed {count}') + elif b != routed or routed is None: + LOG.debug(f'Routed {iCount} - resetting count') + iDelay = 10 + routed = b - status = self.self_get_connection_status() - if not status: - if count % 6 == 1: - LOG.info(f'Not connected {count}') - self.dht_init() + dht_conneted = self.self_get_connection_status() + if not dht_conneted: + self.dht_init() + LOG.info(f'Not DHT connected {iCount} iterating {10 + iDelay} seconds') + self.do(10 + iDelay) + #drop through - if b and not checked and status: + if not group_connected and dht_conneted: LOG.info('Connected to DHT.') - checked = True + group_connected = True try: - self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK) - LOG.info(f'Connected to group {self.bid}') + #? self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK) + r = self.group_reconnect(self.sGROUP_BOT_NUM) + LOG.info(f'Connected to group {r}') except ctypes.ArgumentError as e: self.bid = None - + if self.bid == None: self.ensure_exe(self.friend_add_norequest, self.sGROUP_BOT_PK) LOG.info(f'friend_add_n to group {self.sGROUP_BOT_PK[:8]}') self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK) LOG.info(f'Added to group {self.bid}') - num = self.sGROUP_NUM + num = self.sGROUP_BOT_NUM my_pk = self.group_self_get_public_key(num) LOG.info(f'Connected to group as {my_pk[:8]}') - if b and checked and not status: + if group_connected and not dht_conneted: LOG.info('Disconnected from DHT.') self.dht_init() - checked = False - + group_connected = False + if not self.irc: LOG.info('Disconnected from IRC.') - self.irc_init() + self.irc_init() if not self.irc: - sleep(10) + self.do(20) continue - - LOG.info('Waiting on IRC.') - readable, _, _ = select.select([self.irc], [], [], 0.1) + LOG.info('Waiting on IRC.') + iDelay = 10 + + readable = self.spin(20) if not readable: LOG.info('Waited on IRC but nothing to read.') - else: - self.readbuffer += self.irc.recv(4096) - lines = self.readbuffer.split(b'\n') - self.irc_check(lines) - LOG.info(f'Waited on IRC and got {len(lines)} lines.') - self.readbuffer = lines.pop() - for line in lines: - line = str(line, 'UTF-8') - i = line.find(' ') - print(line[i+1:]) - l = line.rstrip().split() - rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % - self._sChannel, line, re.S) - if rx: - print('IRC> %s: %s' % rx.groups()) - msg = '[%s]: %s' % rx.groups() - content = rx.group(2) + continue + try: + self.irc_readlines() + except Exception as e: + LOG.exception(f'IRC Error during read: {e}') + # close irc? + try: self.irc.close() + except: pass + self.irc = None + self.irc_init() + continue + + return 0 - if content[1:].startswith('ACTION '): - action = '[%s]: %s' % (rx.group(1), - rx.group(2)[8:-1]) - self.ensure_exe(self.group_action_send, - self.tox_group_id, action) - elif self.tox_group_id != None: - self.ensure_exe(self.group_message_send, - self.tox_group_id, msg) - - if content.startswith('^'): - self.handle_command(content) - - elif l[0] == 'PING': - self.irc_send('PONG %s\r\n' % l[1]) - elif l[1] == '376': - # :End of /MOTD command - self.irc.send(bytes('PRIVMSG NickServ :IDENTIFY %s %s\r\n' - % (NICK, PWD,), 'UTF-8')) - self.irc.send(bytes('JOIN %s\r\n' % self._sChannel, 'UTF-8')) - elif l[1] == '421': - # 421 SyniTox .PRIVMSG :Unknown command - pass - elif l[1] == '477': - #477 SyniTox #tor :Cannot join channel (Need to be identified and verified to join this channel, '/msg NickServ help' to learn how to register and verify.) - self.irc.send(bytes('HELP \r\n', 'UTF-8')) - self.irc.send(bytes('MSG NickServ help\r\n', 'UTF-8')) - - pass - - - self.do() - except KeyboardInterrupt: - ret = 0 - except Exception as e: - LOG.exception(f'Error running program:\n{e}', exc_info=True) - ret = 1 - else: - ret = 0 - self.quit() - return ret - def quit(self): self.del_callbacks() self.save_to_file() @@ -448,18 +753,27 @@ class SyniTox(Tox): sleep(1) def on_connection_status(self, friendId, status): + # scs_wrapped if not self.request and not self.joined \ and friendId == self.bid and status: - LOG.info('Groupbot online, trying to join group chat.') + LOG.info('Groupbot online, trying to get invited to group chat.') self.request = True - self.ensure_exe(self.send_message, self.bid, 'invite') + type_ = TOX_MESSAGE_TYPE['NORMAL'] + # the bot is sending a message to myself self.bid + self.ensure_exe(self.friend_send_message, self.bid, type_, 'invite') + # gi_wrapped def on_group_invite(self, friendid, invite_data, user_data): if not self.joined: self.joined = True - self.tox_group_id = self.join_groupchat(friendid, data) + nick = self._oArgs.group_nick + self.tox_group_id = self.group_invite_accept(invite_data, friendid, nick) LOG.info('Joined groupchat.') + def group_peername(self, groupnumber, peer_id): + #dunno + return '' + def on_group_message(self, groupnumber, peer_id, message): name = self.group_peername(groupnumber, peer_id) if len(name) and name != NAME: @@ -468,44 +782,46 @@ class SyniTox(Tox): message = '\x0309%s\x03' % message self.irc_send(b'PRIVMSG %s :[%s]: %s\r\n' % - (self._sChannel, name, message)) + (self._oArgs.irc_chan, name, message)) if message.startswith('^'): self.handle_command(message) def on_group_action(self, groupnumber, peer_id, action): """old? message type action?""" name = self.group_peername(groupnumber, peer_id) - if len(name) and name != NAME: + if name and name != NAME: print('TOX> %s: %s' % (name, action)) if action.startswith('>'): action = '\x0309%s\x03' % action self.irc_send('PRIVMSG %s :\x01ACTION [%s]: %s\x01\r\n' % - (self._sChannel, name, action)) + (self._oArgs.irc_chan, name, action)) def on_friend_request(self, pk, message): LOG.info('Friend request from %s: %s' % (pk, message)) - self.add_friend_norequest(pk) + self.friend_add_norequest(pk) LOG.info('Accepted.') def on_friend_message(self, friendid, message): - if message == 'invite': + if message.startswith('invite'): if not self.tox_group_id is None: - LOG.info('Inviting %s' % self.get_name(friendid)) - self.invite_friend(friendid, self.tox_group_id) + LOG.info('Inviting %s' % self.friend_get_name(friendid)) + self.group_invite_friend(self.sGROUP_BOT_NUM, friendid) return else: message = 'Waiting for GroupBot, please try again in 1 min.' - self.ensure_exe(self.send_message, friendid, message) + type_ = TOX_MESSAGE_TYPE['NORMAL'] + self.ensure_exe(self.friend_send_message, friendid, type_, message) def send_both(self, content): - self.ensure_exe(self.group_message_send, self.tox_group_id, content) - self.irc_send('PRIVMSG %s :%s\r\n' % (self._sChannel, content)) + type_ = TOX_MESSAGE_TYPE['NORMAL'] + self.ensure_exe(self.group_send_message, self.sGROUP_BOT_NUM, type_, content) + self.irc_send('PRIVMSG %s :%s\r\n' % (self._oArgs.irc_chan, content)) def handle_command(self, cmd): cmd = cmd[1:] if cmd in ['syncbot', 'echobot']: - self.send_both(self.get_address()) + self.send_both(self.self_get_address()) elif cmd == 'resync': sys.exit(0) elif cmd.startswith('remember '): @@ -520,22 +836,46 @@ class SyniTox(Tox): elif self.memory.has_key(cmd): self.send_both(self.memory[cmd]) + def is_data_encrypted(self, data): + return len(data) > 0 and self._toxes.is_data_encrypted(data) -def iMain(oArgs): + def pass_encrypt(self, data): + return self._toxes.pass_encrypt(data, self._oArgs.password) + + def has_password(self): + return self._oArgs.password + + def pass_decrypt(self, data): + return self._toxes.pass_decrypt(data, self._oArgs.password) + + +def iMain(oArgs, oOpts): assert oTOX_OPTIONS assert oTOX_OARGS - - sChannel = oArgs.irc_chan - sIRC_HOST = oArgs.irc_host - iIRC_PORT = oArgs.irc_port - o = SyniTox(oTOX_OPTIONS, sChannel, sIRC_HOST, iIRC_PORT) - o.start() - ret = o.iLoop() + try: + o = SyniTox(oArgs, oOpts) + __builtins__.app = o + o.start() + ret = o.iLoop() + except KeyboardInterrupt: + ret = 0 + except ( SSL.Error, ) as e: + LOG.error(f"SSL error: {e.args}") + ret = 1 + except (SSL.SysCallError, ) as e: + # OpenSSL.SSL.SysCallError: (9, 'EBADF') + LOG.error(f"SSL error: {e.args}") + ret = 1 + except Exception as e: + LOG.exception(f'Error running program:\n{e}') + ret = 2 + else: + ret = 0 + o.quit() return ret def oToxygenToxOptions(oArgs): - data = None tox_options = wrapper.tox.Tox.options_new() if oArgs.proxy_type: tox_options.contents.proxy_type = int(oArgs.proxy_type) @@ -561,17 +901,8 @@ def oToxygenToxOptions(oArgs): else: tox_options.contents.ipv6_enabled = bool(oArgs.ipv6_enabled) - if data: # load existing profile - tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['TOX_SAVE'] - tox_options.contents.savedata_data = c_char_p(data) - tox_options.contents.savedata_length = len(data) - else: # create new profile - tox_options.contents.savedata_type = enums.TOX_SAVEDATA_TYPE['NONE'] - tox_options.contents.savedata_data = None - tox_options.contents.savedata_length = 0 - #? tox_options.contents.log_callback = LOG - if tox_options._options_pointer: + if oArgs.trace_enabled and tox_options._options_pointer: # LOG.debug("Adding logging to tox_options._options_pointer ") ts.vAddLoggerCallback(tox_options, ts.on_log) else: @@ -582,11 +913,69 @@ def oToxygenToxOptions(oArgs): def oArgparse(lArgv): parser = ts.oMainArgparser() parser.add_argument('profile', type=str, nargs='?', default=None, - help='Path to Tox profile') - # irc.libera.net #tox will not work over Tor - parser.add_argument('--irc_host', type=str, default='irc.oftc.net') - parser.add_argument('--irc_port', type=int, default=6667) - parser.add_argument('--irc_chan', type=str, default='#tor') + help='Path to Tox profile - new groups will be saved there') + + CAcs = [] + for elt in lCAs: + if os.path.exists(elt): + CAcs.append(elt) + break + + parser.add_argument('--bot_name', type=str, default=bot_toxname) + parser.add_argument('--max_sleep', type=int, default=3600, + help="max time to sleep waiting for routing before exiting") + + parser.add_argument('--password', type=str, default='', + help="password for the profile if encrypted") +# parser.add_argument('--irc_type', type=str, default='', +# choices=['', 'startls', 'direct') + # does host == connect ? + parser.add_argument('--irc_host', type=str, default='irc.oftc.net', + help="irc.libera.chat will not work over Tor") + parser.add_argument('--irc_port', type=int, default=6667, + help="default 6667, but may be 6697 with SSL") + parser.add_argument('--irc_chan', type=str, default='#tor', + help="IRC channel to join - include the #") + # + parser.add_argument('--irc_ssl', type=str, default='', + help="TLS version; empty is no SSL", + choices=['', 'tls1.2', 'tls1.3']) + parser.add_argument('--irc_ca', type=str, + help="Certificate Authority file or directory", + default=CAcs[0]) + parser.add_argument('--irc_pem', type=str, default='', + help="Certificate and key as pem; use openssl req -x509 -nodes -newkey rsa:2048") + parser.add_argument('--irc_fp', type=str, default='', + help="fingerprint of the pem added with CERT ADD; use openssl x509 -noout -fingerprint -SHA1 -text") + parser.add_argument('--irc_nick', type=str, default='', + help="IRC Nickname") + parser.add_argument('--irc_name', type=str, default='', + help="Third field in USER") + parser.add_argument('--irc_ident', type=str, default='', + help="First field in USER") + parser.add_argument('--irc_pass', type=str, default='', + help="password for INDENTIFY or REGISTER") + parser.add_argument('--irc_email', type=str, default='', + help="Use email to REGISTER with _pass") + # + parser.add_argument('--group_pass', type=str, default='', + help="password for the group - optional") + parser.add_argument('--group_state', type=str, default='public', + choices=['public','private'], + help="state for the group - default public") + parser.add_argument('--group_chatid', type=str, default='', + help="chat_id of the group - will be created on first use") + parser.add_argument('--group_name', type=str, default='', + help="name for the group") + parser.add_argument('--group_nick', type=str, default='', + help="Nickname of the group founder") + parser.add_argument('--group_invite', type=str, default='', + help="A PK to invite to the group") + parser.add_argument('--group_moderator', type=str, default='', + help="A PK to invite to the group as moderator") + parser.add_argument('--group_ignore', type=str, default='', + help="A PK to ignore by the group") + oArgs = parser.parse_args(lArgv) for key in ts.lBOOLEANS: @@ -605,17 +994,16 @@ def oArgparse(lArgv): return oArgs def main(lArgs=None): - global oTOX_OARGS - + if lArgs is None: lArgs = [] - oArgs = oArgparse(lArgs) - oTOX_OARGS = oArgs + global oTOX_OARGS + oTOX_OARGS = oArgparse(lArgs) global oTOX_OPTIONS - oTOX_OPTIONS = oToxygenToxOptions(oArgs) - ts.vSetupLogging(oArgs) + oTOX_OPTIONS = oToxygenToxOptions(oTOX_OARGS) + ts.vSetupLogging(oTOX_OARGS) # ts.setup_logging(oArgs) - - return iMain(oArgs) + + return iMain(oTOX_OARGS, oTOX_OPTIONS) if __name__ == '__main__': sys.exit(main(sys.argv[1:])) From 354f4eceb2a71c7ee5372d96c046a2a4190c9866 Mon Sep 17 00:00:00 2001 From: emdee Date: Wed, 26 Oct 2022 09:13:01 +0000 Subject: [PATCH 04/10] Added more groups support --- tox-irc-sync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 9f1145b..20ab935 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -331,6 +331,7 @@ class SyniTox(Tox): nick = self._oArgs.group_nick # is the chat_id the pk? chat_id = self._oArgs.group_chatid + if not chat_id: return -1 num = self.group_join(chat_id, password, nick, status='') self.sGROUP_BOT_NUM = num self.group_self_set_status(num, TOX_USER_STATUS['NONE']) @@ -964,7 +965,7 @@ def oArgparse(lArgv): choices=['public','private'], help="state for the group - default public") parser.add_argument('--group_chatid', type=str, default='', - help="chat_id of the group - will be created on first use") + help="chat_id of the group - leave empty and will be created on first use") parser.add_argument('--group_name', type=str, default='', help="name for the group") parser.add_argument('--group_nick', type=str, default='', From 04985b1fb278147f4db3d8733f820d4d0881b271 Mon Sep 17 00:00:00 2001 From: emdee Date: Sat, 29 Oct 2022 18:44:37 +0000 Subject: [PATCH 05/10] Stem --- README.md | 6 +++--- tox-irc-sync.py | 46 +++++++++++++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7798c63..f894e0f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A bot that sync messages between IRC and Tox NGC group chat. Hard forked from and changed to use the Python wrapping from -. +. Just clone that repo and put the resulting directory on your ```PYTHONPATH```. @@ -144,13 +144,13 @@ python3 tox-irc-sync.py \ ## ChangeLog -* changed to use the Python wrapping from +* changed to use the Python wrapping from * ```tox_irc_sync``` does SSL now. ### Future Directions 1. It's intended as a IRC->Tox NGC gateway but it could work the other way round. -2. It could be a plugin under +2. It could be a plugin under which would broaden the range of callbacks that could be supported. 3. It could be a gateway to an existing NGC group with an invite bot. diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 20ab935..ddd3899 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -14,6 +14,9 @@ from threading import Thread from random import shuffle from OpenSSL import SSL +import warnings +warnings.filterwarnings('ignore') + import wrapper from wrapper.tox import Tox from wrapper.toxav import ToxAV @@ -37,7 +40,9 @@ LOG = logging.getLogger('app.'+'ts') NAME = 'SyniTox' # possible CA locations picks the first one -lCAs = ['/etc/ssl/cacert.pem'] +lCAs = ['/etc/ssl/cacert.pem', + # debian + '/etc/ssl/certs/'] bot_toxname = 'SyniTox' @@ -368,6 +373,7 @@ class SyniTox(Tox): return True def init_callbacks(self): + return # wraps self with LOG.info("Adding Tox init_callbacks") def gi_wrapped(iTox, friendid, invite_data, invite_len, *args): @@ -436,7 +442,16 @@ class SyniTox(Tox): if not self._ssl_context: self.start_ssl(self._oArgs.irc_host) irc = SSL.Connection(self._ssl_context, irc) - irc.connect((self._oArgs.irc_host, self._oArgs.irc_port)) + try: + host = ts.sDNSLookup(self._oArgs.irc_host) + except Exception as e: + LOG.warn(f"{self._oArgs.irc_host} errored in resolve {e}") + host = self._oArgs.irc_host + else: + if not host: + LOG.warn(f"{self._oArgs.irc_host} did not resolve.") + host = self._oArgs.irc_host + irc.connect((host, self._oArgs.irc_port)) irc.do_handshake() LOG.info('IRC SSL connected ') else: @@ -453,10 +468,10 @@ class SyniTox(Tox): LOG.warn(f"Error: {e}") return - self.irc = irc - self.irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) - self.irc.send(bytes('USER %s %s bla :%s\r\n' % ( + irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) + irc.send(bytes('USER %s %s bla :%s\r\n' % ( ident, self._oArgs.irc_host, realname), 'UTF-8')) + self.irc = irc def dht_init(self): if not self.bRouted(): return @@ -681,6 +696,7 @@ class SyniTox(Tox): if not dht_conneted: self.dht_init() LOG.info(f'Not DHT connected {iCount} iterating {10 + iDelay} seconds') + iDelay = iDelay + iDelay // 10 self.do(10 + iDelay) #drop through @@ -715,23 +731,26 @@ class SyniTox(Tox): self.do(20) continue - LOG.info('Waiting on IRC.') - iDelay = 10 + LOG.info(f'Waiting on IRC to {self._oArgs.irc_host} on {self._oArgs.irc_port}') readable = self.spin(20) if not readable: LOG.info('Waited on IRC but nothing to read.') + iDelay = iDelay + iDelay // 10 continue try: self.irc_readlines() except Exception as e: - LOG.exception(f'IRC Error during read: {e}') + LOG.warn(f'IRC Error during read: {e}') # close irc? - try: self.irc.close() + try: + self.irc.close() + self.irc = None except: pass - self.irc = None - self.irc_init() continue + else: + iDelay = 10 + return 0 @@ -750,7 +769,6 @@ class SyniTox(Tox): success = True break except socket.error: - self.irc_init() sleep(1) def on_connection_status(self, friendId, status): @@ -859,6 +877,7 @@ def iMain(oArgs, oOpts): __builtins__.app = o o.start() ret = o.iLoop() + o.quit() except KeyboardInterrupt: ret = 0 except ( SSL.Error, ) as e: @@ -873,7 +892,6 @@ def iMain(oArgs, oOpts): ret = 2 else: ret = 0 - o.quit() return ret def oToxygenToxOptions(oArgs): @@ -922,6 +940,7 @@ def oArgparse(lArgv): CAcs.append(elt) break + parser.add_argument('--log_level', type=int, default=10) parser.add_argument('--bot_name', type=str, default=bot_toxname) parser.add_argument('--max_sleep', type=int, default=3600, help="max time to sleep waiting for routing before exiting") @@ -931,6 +950,7 @@ def oArgparse(lArgv): # parser.add_argument('--irc_type', type=str, default='', # choices=['', 'startls', 'direct') # does host == connect ? + # oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion:6697 parser.add_argument('--irc_host', type=str, default='irc.oftc.net', help="irc.libera.chat will not work over Tor") parser.add_argument('--irc_port', type=int, default=6667, From 3e093a1a41a821b588a6693cb2e68b348f94290e Mon Sep 17 00:00:00 2001 From: emdee Date: Wed, 2 Nov 2022 08:18:52 +0000 Subject: [PATCH 06/10] SSL work --- tox-irc-sync.py | 241 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 161 insertions(+), 80 deletions(-) diff --git a/tox-irc-sync.py b/tox-irc-sync.py index ddd3899..527b070 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -8,6 +8,7 @@ import re import pickle import logging import ctypes +import traceback from time import sleep from threading import Thread @@ -18,6 +19,7 @@ import warnings warnings.filterwarnings('ignore') import wrapper +import wrapper_tests from wrapper.tox import Tox from wrapper.toxav import ToxAV import wrapper.toxcore_enums_and_consts as enums @@ -36,7 +38,7 @@ import wrapper.toxencryptsave as tox_encrypt_save global LOG LOG = logging.getLogger('app.'+'ts') - +class SyniToxError(Exception): pass NAME = 'SyniTox' # possible CA locations picks the first one @@ -60,7 +62,8 @@ def LOG_TRACE(a): if bVERBOSE: print('TRAC> '+a) # https://wiki.python.org/moin/SSL -def ssl_verify_cb(HOST): +def ssl_verify_cb(HOST, override=False): + assert HOST # wrapps host def ssl_verify(*args): """ @@ -68,6 +71,7 @@ def ssl_verify_cb(HOST): should return true if verification passes and false otherwise """ LOG.debug(f"ssl_verify {len(args)} {args}") + if override: return True ssl_conn, x509, error_num, depth, return_code = args if error_num != 0: return False @@ -177,28 +181,38 @@ class SyniTox(Tox): with open(self.sMEMORY_DB, 'r') as f: self.memory = pickle.load(f) - if self._oArgs.irc_ssl != '': - self.start_ssl(self._oArgs.irc_host) - def start_ssl(self, HOST): if not self._ssl_context: + if HOST.endswith('.onion'): + override = True + else: + override = False # TLSv1_3_METHOD does not exist context = SSL.Context(SSL.TLSv1_2_METHOD) context.set_options(SSL.OP_NO_SSLv2|SSL.OP_NO_SSLv3|SSL.OP_NO_TLSv1) if self._oArgs.irc_pem: + key = self._oArgs.irc_pem + assert os.path.exists(key), key val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT LOG.info('Using keyfile: %s' % self._oArgs.irc_pem) - context.use_privatekey_file(self._oArgs.irc_pem) + if False: + context.use_certificate_file(key, filetype=SSL.FILETYPE_PEM) + if True: + # key = self._oArgs.irc_pem.replace('.pem', '.key') + assert os.path.exists(key), key + context.use_privatekey_file(key, filetype=SSL.FILETYPE_PEM) else: val = SSL.VERIFY_PEER - context.set_verify(val, ssl_verify_cb(self._oArgs.irc_host)) + context.set_verify(val, ssl_verify_cb(HOST, override)) assert os.path.exists(self._oArgs.irc_ca), self._oArgs.irc_ca if os.path.isdir(self._oArgs.irc_ca): context.load_verify_locations(capath=self._oArgs.irc_ca) else: context.load_verify_locations(cafile=self._oArgs.irc_ca) - if self._oArgs.irc_ssl == 'tls1.2': + if False: + pass + elif self._oArgs.irc_ssl == 'tls1.2': context.set_min_proto_version(SSL.TLS1_2_VERSION) elif self._oArgs.irc_ssl == 'tls1.3': context.set_min_proto_version(SSL.TLS1_3_VERSION) @@ -345,30 +359,30 @@ class SyniTox(Tox): def init_groups(self): LOG.debug(f"init_groups proxy={self._oArgs.proxy_type}") group_name = self._oArgs.bot_name +' Test ' +self._oArgs.irc_chan - if self.sGROUP_BOT_NUM < 0: - # ToDo: look for the first group of the profile - i = self.group_get_number_groups() - if i == 0: - if not self.bRouted(): return False - num = self.create_group() - self.sGROUP_BOT_NUM = num - elif i > 1: - LOG.error('There are more than one groups in this profile') - for ig in range(i): - LOG.warn(f"group #{ig} {self.group_self_get_name(ig)}") - raise RuntimeError("select one of the groups at the cmdline") - else: - if not self.bRouted(): return False - num = self.join_group() - - LOG.info(f"init_groups GROUP_BOT_PK={self.sGROUP_BOT_PK}") - - if self.bRouted(): - try: - self.start_groups() - except Exception as e: - LOG.warn(f"init_groups self.start_groups {e}") - return False + if not self.bRouted(): return + try: + if self.sGROUP_BOT_NUM < 0: + # ToDo: look for the first group of the profile + i = self.group_get_number_groups() + if i == 0: + if not self.bRouted(): return False + num = self.create_group() + self.sGROUP_BOT_NUM = num + elif i > 1: + LOG.error('There are more than one groups in this profile') + for ig in range(i): + LOG.warn(f"group #{ig} {self.group_self_get_name(ig)}") + raise RuntimeError("select one of the groups at the cmdline") + else: + if not self.bRouted(): return False + num = self.join_group() + + LOG.info(f"init_groups GROUP_BOT_PK={self.sGROUP_BOT_PK}") + + self.start_groups() + except Exception as e: + LOG.warn(f"init_groups self.start_groups {e}") + return False # TOX_GROUP_ROLE['FOUNDER'] return True @@ -431,47 +445,85 @@ class SyniTox(Tox): self._oArgs.proxy_host, self._oArgs.proxy_port) irc = socks.socksocket() + iTIMEOUT = 15 elif self._oArgs.proxy_type == 1: socks.setdefaultproxy(socks.PROXY_TYPE_HTTP, self._oArgs.proxy_host, self._oArgs.proxy_port) irc = socks.socksocket() + iTIMEOUT = 15 else: irc = socket.socket() + iTIMEOUT = 10 + try: + ip = ts.sDNSLookup(self._oArgs.irc_connect) + except Exception as e: + LOG.warn(f"{self._oArgs.irc_host} errored in resolve {e}") + ip = self._oArgs.irc_connect + else: + if not ip: + LOG.warn(f"{self._oArgs.irc_host} did not resolve.") + ip = self._oArgs.irc_connect + # https://github.com/pyca/pyopenssl/issues/168 if self._oArgs.irc_ssl: if not self._ssl_context: - self.start_ssl(self._oArgs.irc_host) + self.start_ssl(self._oArgs.irc_connect) irc = SSL.Connection(self._ssl_context, irc) - try: - host = ts.sDNSLookup(self._oArgs.irc_host) - except Exception as e: - LOG.warn(f"{self._oArgs.irc_host} errored in resolve {e}") - host = self._oArgs.irc_host + irc.connect((ip, self._oArgs.irc_port)) + if ip.endswith('.onion'): + irc.set_tlsext_host_name(None) else: - if not host: - LOG.warn(f"{self._oArgs.irc_host} did not resolve.") - host = self._oArgs.irc_host - irc.connect((host, self._oArgs.irc_port)) - irc.do_handshake() - LOG.info('IRC SSL connected ') + irc.set_tlsext_host_name(bytes(self._oArgs.irc_host, 'UTF-8')) + irc.set_connect_state() + while True: + try: + irc.do_handshake() + except SSl.WantReadError: + rd,_,_ = select.select([irc], [], [], irc.gettimeout()) + if not rd: + raise socket.timeout('timeout') + continue + except SSl.Error as e: + raise + break + for cert in irc.get_peer_cert_chain(): + print(f"{cert.get_subject} {cert.get_issuer}") else: - irc.connect((self._oArgs.irc_host, self._oArgs.irc_port)) - LOG.info('IRC connected ') - + irc.connect((ip, self._oArgs.irc_port)) + LOG.info(f"IRC {'SSL ' if self._oArgs.irc_ssl else ''} connected ") + + except wrapper_tests.socks.Socks5Error as e: + if len(e.args[0]) == 2 and e.args[0][0] ==2: + LOG.warn(f"Socks5Error: do you have Tor SafeSocks set? {e.args[0]}") + return + else: + LOG.error(f"Socks5Error: {e.args}") + raise SyniToxError(f"{e.args}") + except socket.timeout as e: + LOG.warn(f"socket error: {e.args}") + return except ( SSL.Error, ) as e: LOG.warn(f"SSL error: {e.args}") return except (SSL.SysCallError, ) as e: - LOG.warn(f"SSL error: {e.args}") + LOG.warn(f"SSLSyscall error: {e.args}") + LOG.warn(traceback.format_exc()) return + except wrapper_tests.socks.Socks5Error as e: + # (2, 'connection not allowed by ruleset') + raise except Exception as e: LOG.warn(f"Error: {e}") + LOG.warn(traceback.format_exc()) return - irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) - irc.send(bytes('USER %s %s bla :%s\r\n' % ( - ident, self._oArgs.irc_host, realname), 'UTF-8')) self.irc = irc + if not self._oArgs.irc_ssl: + self.irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) + self.irc.send(bytes('USER %s %s bla :%s\r\n' % ( + self._oArgs.irc_ident, + self._oArgs.irc_host, + self._oArgs.irc_name), 'UTF-8')) def dht_init(self): if not self.bRouted(): return @@ -489,7 +541,7 @@ class SyniTox(Tox): self.test_net() lNodes = self._settings['current_nodes_tcp'] shuffle(lNodes) - LOG.info(f'TCP bootstapping 6') + LOG.debug(f'TCP bootstapping 6') ts.bootstrap_tcp(lNodes[:6], [self]) def get_all_groups(self): @@ -566,9 +618,9 @@ class SyniTox(Tox): for line in lines[:5]: line = str(line, 'UTF-8').strip().lower() if 'banned' in line: - raise RuntimeError(line) + raise SyniToxError(line) if 'error' in line and 'closing' in line: - raise RuntimeError(line) + raise SyniToxError(line) def irc_readlines(self): nick = self._oArgs.irc_nick @@ -598,18 +650,29 @@ class SyniTox(Tox): self.irc_send('PONG %s\r\n' % l[1]) elif len(l) < 2: pass - elif l[1] == '376': + elif l[1] in ['461', '431']: + pass + elif l[1] in ['433', '462', '477']: + if self._oArgs.irc_ssl: + LOG.warn("Maybe the certificate was not received") + raise SyniToxError(line) + elif l[1] in ['376']: # :End of /MOTD command - if email == '': - self.irc.send(bytes('PRIVMSG NickServ IDENTIFY %s %s\r\n' + if self._oArgs.irc_ssl != '': + pass + elif email == '' and pwd: + LOG.info(bytes('PRIVMSG NickServ IDENTIFY %s %s\r\n' % (nick, pwd,), 'UTF-8')) - else: + self.irc.send(bytes('PRIVMSG NickServ IDENTIFY %s %s\r\n' + % (nick, pwd,), 'UTF-8')) + elif email != '' and pwd: + LOG.info(bytes('PRIVMSG NickServ REGISTER %s %s\r\n' + % (pwd, email,), 'UTF-8')) self.irc.send(bytes('PRIVMSG NickServ REGISTER %s %s\r\n' % (pwd, email,), 'UTF-8')) - if False and fp: - LOG.info(f"PRIVMSG NickServ CERT ADD") -# self.irc.send(bytes(f'PRIVMSG NickServ CERT ADD {fp}\r\n', 'UTF-8')) - # + else: + LOG.error("you must provide a password to register") + raise RuntimeError("you must provide a password to register") self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) # put off init_groups until you have joined IRC self.init_groups() @@ -651,14 +714,15 @@ class SyniTox(Tox): if content.startswith('^'): self.handle_command(content) - def spin(self, n=20): + def spin(self, n=20, iMax=1000): readable = False waiti = 0 while not readable: waiti += 1 - readable, _, _ = select.select([self.irc], [], [], n/1000.0 ) + readable, _, _ = select.select([self.irc], [], [], n/100.0 ) + if readable and len(readable) and readable[0]: return readable self.do(n) - if waiti > 100: break + if waiti > iMax: break return readable def iLoop(self): @@ -670,6 +734,8 @@ class SyniTox(Tox): iDelay = 10 nick = self._oArgs.irc_nick + realname = self._oArgs.irc_name + ident = self._oArgs.irc_ident pwd = self._oArgs.irc_pass email = self._oArgs.irc_email LOG.info(f"Looping for Tox and IRC connections") @@ -697,7 +763,7 @@ class SyniTox(Tox): self.dht_init() LOG.info(f'Not DHT connected {iCount} iterating {10 + iDelay} seconds') iDelay = iDelay + iDelay // 10 - self.do(10 + iDelay) + self.do(iDelay) #drop through if not group_connected and dht_conneted: @@ -725,32 +791,38 @@ class SyniTox(Tox): group_connected = False if not self.irc: - LOG.info('Disconnected from IRC.') self.irc_init() if not self.irc: self.do(20) continue + LOG.info(f'Waiting on IRC to {self._oArgs.irc_host} on {self._oArgs.irc_port}') readable = self.spin(20) - if not readable: + if not readable or not readable[0]: LOG.info('Waited on IRC but nothing to read.') iDelay = iDelay + iDelay // 10 continue try: - self.irc_readlines() + pass except Exception as e: - LOG.warn(f'IRC Error during read: {e}') - # close irc? - try: - self.irc.close() - self.irc = None - except: pass - continue - else: - iDelay = 10 + if len(e.args) > 1 and e.args[0] == 32: + raise + elif f"{e}" != "2": + LOG.warn(f'IRC Error during read: {e}') + # close irc? + try: + self.irc.close() + self.irc = None + except: pass + continue + else: + iDelay = 10 + else: + iDelay = 10 + self.irc_readlines() return 0 @@ -887,9 +959,12 @@ def iMain(oArgs, oOpts): # OpenSSL.SSL.SysCallError: (9, 'EBADF') LOG.error(f"SSL error: {e.args}") ret = 1 + except SyniToxError as e: + LOG.error(f'Error running program:\n{e}') + ret = 2 except Exception as e: LOG.exception(f'Error running program:\n{e}') - ret = 2 + ret = 3 else: ret = 0 return ret @@ -951,8 +1026,11 @@ def oArgparse(lArgv): # choices=['', 'startls', 'direct') # does host == connect ? # oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion:6697 - parser.add_argument('--irc_host', type=str, default='irc.oftc.net', + # irc.oftc.net + parser.add_argument('--irc_host', type=str, default='', help="irc.libera.chat will not work over Tor") + parser.add_argument('--irc_connect', type=str, default='', + help="defaults to irc_host") parser.add_argument('--irc_port', type=int, default=6667, help="default 6667, but may be 6697 with SSL") parser.add_argument('--irc_chan', type=str, default='#tor', @@ -1019,6 +1097,9 @@ def main(lArgs=None): if lArgs is None: lArgs = [] global oTOX_OARGS oTOX_OARGS = oArgparse(lArgs) + assert oTOX_OARGS.irc_host or oTOX_OARGS.irc_connect + if not oTOX_OARGS.irc_connect: + oTOX_OARGS.irc_connect = oTOX_OARGS.irc_host global oTOX_OPTIONS oTOX_OPTIONS = oToxygenToxOptions(oTOX_OARGS) ts.vSetupLogging(oTOX_OARGS) From 5ff9bd0680b978599d9e198dfd3295a350c596a8 Mon Sep 17 00:00:00 2001 From: emdee Date: Thu, 3 Nov 2022 02:51:14 +0000 Subject: [PATCH 07/10] SSL 1.3 maybe --- tox-irc-sync.py | 183 ++++++++++++++++++++++++++++++++--------- tox-irc-sync_test.bash | 111 +++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 39 deletions(-) create mode 100644 tox-irc-sync_test.bash diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 527b070..0712894 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -13,6 +13,7 @@ import traceback from time import sleep from threading import Thread from random import shuffle +from errno import errorcode from OpenSSL import SSL import warnings @@ -38,15 +39,42 @@ import wrapper.toxencryptsave as tox_encrypt_save global LOG LOG = logging.getLogger('app.'+'ts') -class SyniToxError(Exception): pass +class SyniToxError(BaseException): pass NAME = 'SyniTox' +SSL_TOR_RANGE = '172.' # possible CA locations picks the first one -lCAs = ['/etc/ssl/cacert.pem', - # debian - '/etc/ssl/certs/'] - +lCAs = [# debian and gentoo + '/etc/ssl/certs/', + ] +lCAfs = SSL._CERTIFICATE_FILE_LOCATIONS +# openssl ciphers -s -v|grep 1.3 > /tmp/v1.3 +lOPENSSL_13_CIPHERS = ['TLS_AES_256_GCM_SHA384', + 'TLS_CHACHA20_POLY1305_SHA256', + 'TLS_AES_128_GCM_SHA256'] +lOPENSSL_12_CIPHERS = ['ECDHE-ECDSA-AES256-GCM-SHA384', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'DHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-CHACHA20-POLY1305', + 'ECDHE-RSA-CHACHA20-POLY1305', + 'DHE-RSA-CHACHA20-POLY1305', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES128-GCM-SHA256', + 'DHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA384', + 'DHE-RSA-AES256-SHA256', + 'ECDHE-ECDSA-AES128-SHA256', + 'ECDHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA256', + 'AES256-GCM-SHA384', + 'AES128-GCM-SHA256', + 'AES256-SHA256', + 'AES128-SHA256' + ] bot_toxname = 'SyniTox' +iSocks5ErrorMax = 5 +iSocks5Error = 0 # tox.py can be called by callbacks def LOG_ERROR(a): print('EROR> '+a) @@ -62,8 +90,8 @@ def LOG_TRACE(a): if bVERBOSE: print('TRAC> '+a) # https://wiki.python.org/moin/SSL -def ssl_verify_cb(HOST, override=False): - assert HOST +def ssl_verify_cb(host, override=False): + assert host # wrapps host def ssl_verify(*args): """ @@ -71,28 +99,33 @@ def ssl_verify_cb(HOST, override=False): should return true if verification passes and false otherwise """ LOG.debug(f"ssl_verify {len(args)} {args}") + + # app.ts WARNING SSL error: ([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],) # on .onion - fair enough if override: return True + ssl_conn, x509, error_num, depth, return_code = args if error_num != 0: + LOG.warn(f"ssl_verify error_num={error_num} {errorcode.get(error_num)}") return False if depth != 0: # don't validate names of root certificates return True - if x509.get_subject().commonName == HOST: + if x509.get_subject().commonName == host: return True - LOG.warn(f"ssl_verify {x509.get_subject().commonName} {HOST}") # allow matching subdomains - have , want = x509.get_subject().commonName, HOST + have , want = x509.get_subject().commonName, host if len(have.split('.')) == len(want.split('.')) and len(want.split('.')) > 2: if have.split('.')[1:] == want.split('.')[1:]: + LOG.warn(f"ssl_verify accepting {x509.get_subject().commonName} for {host}") return True return False return ssl_verify + class SyniTox(Tox): def __init__(self, @@ -183,38 +216,61 @@ class SyniTox(Tox): def start_ssl(self, HOST): if not self._ssl_context: - if HOST.endswith('.onion'): + try: + OP_NO_TLSv1_3 = SSL._lib.SSL_OP_NO_TLSv1_3 + except AttributeError: + if self._oArgs.irc_ssl == 'tlsv1.3': + LOG.warning("SSL._lib.SSL_OP_NO_TLSv1_3 is not supported") + LOG.warning("Downgrading SSL to tlsv1.2 ") + self._oArgs.irc_ssl = 'tlsv1.2' + else: + LOG.debug("SSL._lib.SSL_OP_NO_TLSv1_3 is not supported") + else: + LOG.debug("SSL._lib.SSL_OP_NO_TLSv1_3 is supported") + + if self._oArgs.irc_connect.endswith('.onion') or \ + self._oArgs.irc_connect.startswith(SSL_TOR_RANGE): override = True else: override = False # TLSv1_3_METHOD does not exist context = SSL.Context(SSL.TLSv1_2_METHOD) + # SSL.OP_NO_TLSv1_1 is allowed context.set_options(SSL.OP_NO_SSLv2|SSL.OP_NO_SSLv3|SSL.OP_NO_TLSv1) + # this maybe necessary even for a 1.3 site to get the handshake + # in pyOpenSSL - or was it a protocol downgrade attack? +#? context.set_cipher_list("DEFAULT:SECLEVEL=1") + # im getting SSL error: ([('SSL routines', 'tls_construct_client_hello', 'no protocols available')],) + # if I use tlsv1.3 or tlsv1.2 without this on a tlsv1.3 capacble site + if self._oArgs.irc_pem: key = self._oArgs.irc_pem assert os.path.exists(key), key val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT LOG.info('Using keyfile: %s' % self._oArgs.irc_pem) - if False: + if True: + key = self._oArgs.irc_pem.replace('.pem', '.crt') context.use_certificate_file(key, filetype=SSL.FILETYPE_PEM) if True: - # key = self._oArgs.irc_pem.replace('.pem', '.key') + key = self._oArgs.irc_pem.replace('.pem', '.key') assert os.path.exists(key), key context.use_privatekey_file(key, filetype=SSL.FILETYPE_PEM) else: val = SSL.VERIFY_PEER context.set_verify(val, ssl_verify_cb(HOST, override)) - assert os.path.exists(self._oArgs.irc_ca), self._oArgs.irc_ca - if os.path.isdir(self._oArgs.irc_ca): - context.load_verify_locations(capath=self._oArgs.irc_ca) - else: - context.load_verify_locations(cafile=self._oArgs.irc_ca) - if False: - pass - elif self._oArgs.irc_ssl == 'tls1.2': + if self._oArgs.irc_cafile: + # context.load_verify_locations(capath=self._oArgs.irc_ca) + context.load_verify_locations(self._oArgs.irc_cafile, capath=self._oArgs.irc_cadir) + elif self._oArgs.irc_cadir: + context.load_verify_locations(None, capath=self._oArgs.irc_cadir) + if self._oArgs.irc_ssl == 'tlsv1.1': + context.set_min_proto_version(SSL.TLS1_1_VERSION) + elif self._oArgs.irc_ssl == 'tlsv1.2': + context.set_cipher_list(bytes(' '.join(lOPENSSL_12_CIPHERS), 'UTF-8')) context.set_min_proto_version(SSL.TLS1_2_VERSION) - elif self._oArgs.irc_ssl == 'tls1.3': + elif self._oArgs.irc_ssl == 'tlsv1.3': +#? context.set_cipher_list(bytes(' '.join(lOPENSSL_13_CIPHERS), 'UTF-8')) context.set_min_proto_version(SSL.TLS1_3_VERSION) self._ssl_context = context @@ -432,13 +488,35 @@ class SyniTox(Tox): self.callback_friend_request(None) self.callback_friend_request(None) + def diagnose_ciphers(self, irc): + cipher_name = irc.get_cipher_name() + LOG.info(f"cipher_name={irc.get_cipher_name()}") + LOG.debug(f"get_cipher_list={irc.get_cipher_list()}") + cipher_list=irc.get_cipher_list() + for ci in lOPENSSL_13_CIPHERS: + if ci in cipher_list: LOG.info(f"server supports v1.3 cipher {ci}") + cipher_name = irc.get_cipher_name() + LOG.info(f"cipher_name={irc.get_cipher_name()}") + if self._oArgs.irc_ssl == 'tlsv1.2': + assert cipher_name in lOPENSSL_12_CIPHERS, cipher_name + elif self._oArgs.irc_ssl == 'tlsv1.3': + assert cipher_name in lOPENSSL_13_CIPHERS, cipher_name + + for cert in irc.get_peer_cert_chain(): + # x509 objects - just want the /CN + LOG.debug(f"{cert.get_subject()} {cert.get_issuer()}") + assert irc.get_protocol_version_name().lower() == \ + self._oArgs.irc_ssl, \ + irc.get_protocol_version_name().lower() + def irc_init(self): + global iSocks5Error + if not self.bRouted(): return nick = self._oArgs.irc_nick realname = self._oArgs.irc_name ident = self._oArgs.irc_ident - - LOG.info(f"irc_init proxy={self._oArgs.proxy_type}") + LOG.info(f"irc_init proxy={self._oArgs.proxy_type} SSL={self._oArgs.irc_ssl}") try: if self._oArgs.proxy_type == 2: socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, @@ -474,27 +552,37 @@ class SyniTox(Tox): irc.set_tlsext_host_name(None) else: irc.set_tlsext_host_name(bytes(self._oArgs.irc_host, 'UTF-8')) - irc.set_connect_state() +#? irc.set_connect_state() while True: try: irc.do_handshake() - except SSl.WantReadError: + except SSL.WantReadError: rd,_,_ = select.select([irc], [], [], irc.gettimeout()) if not rd: raise socket.timeout('timeout') continue - except SSl.Error as e: + except SSL.Error as e: raise break - for cert in irc.get_peer_cert_chain(): - print(f"{cert.get_subject} {cert.get_issuer}") + self.diagnose_ciphers(irc) else: irc.connect((ip, self._oArgs.irc_port)) LOG.info(f"IRC {'SSL ' if self._oArgs.irc_ssl else ''} connected ") except wrapper_tests.socks.Socks5Error as e: - if len(e.args[0]) == 2 and e.args[0][0] ==2: + iSocks5Error += 1 + if iSocks5Error >= iSocks5ErrorMax: + raise SyniToxError(f"{e.args}") + if len(e.args[0]) == 2 and e.args[0][0] == 2: LOG.warn(f"Socks5Error: do you have Tor SafeSocks set? {e.args[0]}") + elif len(e.args[0]) == 2 and e.args[0][0] == 5: + # (5, 'Connection refused') + LOG.warn(f"Socks5Error: do you have Tor running? {e.args[0]}") + raise SyniToxError(f"{e.args}") + elif len(e.args[0]) == 2 and e.args[0][0] in [1, 6]: + # (6, 'TTL expired'), 1, ('general SOCKS server failure') + # Missing mapping for virtual address '172.17.140.117'. Refusing. + LOG.warn(f"Socks5Error: {e.args[0]}") return else: LOG.error(f"Socks5Error: {e.args}") @@ -502,16 +590,18 @@ class SyniTox(Tox): except socket.timeout as e: LOG.warn(f"socket error: {e.args}") return + except ( ConnectionRefusedError) as e: + raise SyniToxError(f"{e.args}") except ( SSL.Error, ) as e: + iSocks5Error += 1 + if iSocks5Error >= iSocks5ErrorMax: + raise SyniToxError(f"{e.args}") LOG.warn(f"SSL error: {e.args}") return except (SSL.SysCallError, ) as e: LOG.warn(f"SSLSyscall error: {e.args}") LOG.warn(traceback.format_exc()) return - except wrapper_tests.socks.Socks5Error as e: - # (2, 'connection not allowed by ruleset') - raise except Exception as e: LOG.warn(f"Error: {e}") LOG.warn(traceback.format_exc()) @@ -524,7 +614,8 @@ class SyniTox(Tox): self._oArgs.irc_ident, self._oArgs.irc_host, self._oArgs.irc_name), 'UTF-8')) - + # OSError: [Errno 9] Bad file descriptor + def dht_init(self): if not self.bRouted(): return if 'current_nodes_udp' not in self._settings: @@ -673,7 +764,11 @@ class SyniTox(Tox): else: LOG.error("you must provide a password to register") raise RuntimeError("you must provide a password to register") - self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) + try: + self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) + except BrokenPipeError: + raise SyniToxError('BrokenPipeError') + # put off init_groups until you have joined IRC self.init_groups() # Make sure we are in @@ -1013,7 +1108,10 @@ def oArgparse(lArgv): for elt in lCAs: if os.path.exists(elt): CAcs.append(elt) - break + CAfs = [] + for elt in lCAfs: + if os.path.exists(elt): + CAfs.append(elt) parser.add_argument('--log_level', type=int, default=10) parser.add_argument('--bot_name', type=str, default=bot_toxname) @@ -1038,9 +1136,12 @@ def oArgparse(lArgv): # parser.add_argument('--irc_ssl', type=str, default='', help="TLS version; empty is no SSL", - choices=['', 'tls1.2', 'tls1.3']) - parser.add_argument('--irc_ca', type=str, - help="Certificate Authority file or directory", + choices=['', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3']) + parser.add_argument('--irc_cafile', type=str, + help="Certificate Authority file", + default=CAfs[0]) + parser.add_argument('--irc_cadir', type=str, + help="Certificate Authority directory", default=CAcs[0]) parser.add_argument('--irc_pem', type=str, default='', help="Certificate and key as pem; use openssl req -x509 -nodes -newkey rsa:2048") @@ -1100,6 +1201,10 @@ def main(lArgs=None): assert oTOX_OARGS.irc_host or oTOX_OARGS.irc_connect if not oTOX_OARGS.irc_connect: oTOX_OARGS.irc_connect = oTOX_OARGS.irc_host + if oTOX_OARGS.irc_cadir: + assert os.path.isdir(oTOX_OARGS.irc_cadir) + if oTOX_OARGS.irc_cafile: + assert os.path.isfile(oTOX_OARGS.irc_cafile) global oTOX_OPTIONS oTOX_OPTIONS = oToxygenToxOptions(oTOX_OARGS) ts.vSetupLogging(oTOX_OARGS) diff --git a/tox-irc-sync_test.bash b/tox-irc-sync_test.bash new file mode 100644 index 0000000..71203ae --- /dev/null +++ b/tox-irc-sync_test.bash @@ -0,0 +1,111 @@ +#!/bin/bash + +#export LD_LIBRARY_PATH=/usr/local/lib +#export TOXCORE_LIBS=/mnt/linuxPen19/var/local/src/c-toxcore/_build +export TOXCORE_LIBS=/mnt/o/var/local/src/tox_profile/libs +export PYTHONPATH=/mnt/o/var/local/src/toxygen_wrapper.git/ + +[ -f /usr/local/bin/usr_local_tput.bash ] && \ + . /usr/local/bin/usr_local_tput.bash || { + DBUG() { echo DEBUG $* ; } + INFO() { echo INFO $* ; } + WARN() { echo WARN $* ; } + ERROR() { echo ERROR $* ; } + } + +TLS=2 +a=`openssl ciphers -s -v|grep -c v1.3` +if [ "$a" -lt 3 ] ; then + WARN no SSSL TLSv1.3 ciphers available to the client. + TLS=2 +fi + +declare -a RARGS +RARGS=( + --log_level 10 +) +[ -n "$socks_proxy" ] && \ +RARGS+=( + --proxy_type 2 + --proxy_port 9050 + --proxy_host 127.0.0.1 +) +declare -a LARGS +LARGS=( + --irc_host irc.oftc.net + --irc_port 7000 + --irc_ssl "" + --irc_ident SyniTox + --irc_name SyniTox + --irc_nick SyniTox + --irc_pass password + ) +DBUG $? + +if [ $# -eq 0 -o "$1" = 1 ] ; then + INFO No SSL + python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" "$@" + DBUG $? +fi + +CIPHER_DOWNGRADE_OVER_TOR=" + +Nmap scan report for irc.oftc.net (130.239.18.116) +Host is up (0.26s latency). +Other addresses for irc.oftc.net (not scanned): (null) +rDNS record for 130.239.18.116: solenoid.acc.umu.se + +PORT STATE SERVICE +6697/tcp open ircs-u +| ssl-enum-ciphers: +| TLSv1.0: +| ciphers: +| TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A +| compressors: +| cipher preference: indeterminate +| cipher preference error: Too few ciphers supported +|_ least strength: A +" + # I know that site does v1.3 3 ciphers +if [ $# -eq 0 -o "$1" = 2 ] ; then + nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p 6697 irc.oftc.net + + # oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion + # irc.oftc.net + LARGS=( + --irc_host irc.oftc.net + --irc_port 6697 + --irc_ssl tlsv1.$TLS + --irc_ident SyniTox + --irc_name SyniTox + --irc_nick SyniTox + --irc_pass password + --irc_pem $HOME/.config/ssl/irc.oftc.net/SyniTox.pem + # E178E7B9BD9E540278118193AD2C84DEF9B35E85 + --irc_fp $HOME/.config/ssl/irc.oftc.net/SyniTox.fp + --irc_cadir '/etc/ssl/certs' + --irc_cafile /etc/ssl/cacert.pem + ) + DBUG $? +fi + +if [ $# -eq 0 -o "$1" = 2 ] ; then + INFO SSL + python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" "$@" +fi + +ip=oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion +if [ $# -eq 0 -o "$1" = 3 ] ; then + nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p 6697 $ip + INFO Onion + python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" "$@" + DBUG $? +fi + +ip=`tor-resolve -4 $ip` +if [ $? -eq 0 -a -n "$ip" ] && [ $# -eq 0 -o "$1" = 4 ] ; then + nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p 6697 $ip + INFO IP $ip + python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" "$@" + DBUG $? +fi From 6f73f766ac8e37d054d058ebf7239c5501196c9c Mon Sep 17 00:00:00 2001 From: emdee Date: Thu, 3 Nov 2022 05:31:50 +0000 Subject: [PATCH 08/10] Fixed TLS1.3 --- tox-irc-sync.py | 26 ++++++++---- tox-irc-sync_test.bash | 95 +++++++++++++++++++++++++++++------------- 2 files changed, 85 insertions(+), 36 deletions(-) diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 0712894..c3431cc 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -234,7 +234,7 @@ class SyniTox(Tox): else: override = False # TLSv1_3_METHOD does not exist - context = SSL.Context(SSL.TLSv1_2_METHOD) + context = SSL.Context(SSL.TLS_CLIENT_METHOD) # TLSv1_2_METHOD # SSL.OP_NO_TLSv1_1 is allowed context.set_options(SSL.OP_NO_SSLv2|SSL.OP_NO_SSLv3|SSL.OP_NO_TLSv1) # this maybe necessary even for a 1.3 site to get the handshake @@ -249,12 +249,22 @@ class SyniTox(Tox): val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT LOG.info('Using keyfile: %s' % self._oArgs.irc_pem) if True: - key = self._oArgs.irc_pem.replace('.pem', '.crt') + # key = self._oArgs.irc_pem.replace('.pem', '.crt') + assert os.path.exists(key), key context.use_certificate_file(key, filetype=SSL.FILETYPE_PEM) if True: - key = self._oArgs.irc_pem.replace('.pem', '.key') + # key = self._oArgs.irc_pem.replace('.pem', '.key') assert os.path.exists(key), key context.use_privatekey_file(key, filetype=SSL.FILETYPE_PEM) + #? load_client_ca + def SSL_hands_cb(oConn,iLine,iRet): + # where in the SSL handshake the function was called, and + # the return code from a internal function call + print(f"iLine={iLine}, iRet={iRet}") + # context.set_info_callback(SSL_hands_cb) + def keylog_callback(oConn,s): + print(s) + context.set_keylog_callback(keylog_callback) else: val = SSL.VERIFY_PEER context.set_verify(val, ssl_verify_cb(HOST, override)) @@ -267,10 +277,10 @@ class SyniTox(Tox): if self._oArgs.irc_ssl == 'tlsv1.1': context.set_min_proto_version(SSL.TLS1_1_VERSION) elif self._oArgs.irc_ssl == 'tlsv1.2': - context.set_cipher_list(bytes(' '.join(lOPENSSL_12_CIPHERS), 'UTF-8')) + context.set_cipher_list(bytes(':'.join(['DEFAULT@SECLEVEL=1']+lOPENSSL_12_CIPHERS), 'UTF-8')) context.set_min_proto_version(SSL.TLS1_2_VERSION) elif self._oArgs.irc_ssl == 'tlsv1.3': -#? context.set_cipher_list(bytes(' '.join(lOPENSSL_13_CIPHERS), 'UTF-8')) + context.set_cipher_list(bytes(':'.join(['DEFAULT@SECLEVEL=1']+lOPENSSL_13_CIPHERS), 'UTF-8')) context.set_min_proto_version(SSL.TLS1_3_VERSION) self._ssl_context = context @@ -567,7 +577,7 @@ class SyniTox(Tox): self.diagnose_ciphers(irc) else: irc.connect((ip, self._oArgs.irc_port)) - LOG.info(f"IRC {'SSL ' if self._oArgs.irc_ssl else ''} connected ") + LOG.info(f"IRC SSL={self._oArgs.irc_ssl} connected ") except wrapper_tests.socks.Socks5Error as e: iSocks5Error += 1 @@ -614,6 +624,7 @@ class SyniTox(Tox): self._oArgs.irc_ident, self._oArgs.irc_host, self._oArgs.irc_name), 'UTF-8')) + # OSError: [Errno 9] Bad file descriptor def dht_init(self): @@ -732,7 +743,8 @@ class SyniTox(Tox): elif l[1] not in ['372']: i = line.find(' ') print(line[i+1:]) - + else: + LOG.info('MOTD') rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % self._oArgs.irc_chan, line, re.S) if rx: diff --git a/tox-irc-sync_test.bash b/tox-irc-sync_test.bash index 71203ae..c35192a 100644 --- a/tox-irc-sync_test.bash +++ b/tox-irc-sync_test.bash @@ -1,4 +1,5 @@ #!/bin/bash +# -*- mode: sh; fill-column: 75; tab-width: 8; coding: utf-8-unix -*- #export LD_LIBRARY_PATH=/usr/local/lib #export TOXCORE_LIBS=/mnt/linuxPen19/var/local/src/c-toxcore/_build @@ -13,13 +14,50 @@ export PYTHONPATH=/mnt/o/var/local/src/toxygen_wrapper.git/ ERROR() { echo ERROR $* ; } } -TLS=2 +HOST=irc.oftc.net +IRC_PORT=6667 +IRCS_PORT=6697 +ONION=oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion + +TLS=0 a=`openssl ciphers -s -v|grep -c v1.3` if [ "$a" -lt 3 ] ; then WARN no SSSL TLSv1.3 ciphers available to the client. TLS=2 +elif nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p $IRCS_PORT $HOST | grep -q 'TLSv1.3:' ; then + TLS=3 +else + TLS=2 +fi +TLS=3 + +if [ "$TLS" -ne 0 ] ; then + SD=$HOME/.config/ssl/$HOST + [ -d $SD ] || mkdir -p $SD || exit 2 + if [ ! -s $SD/$nick.key ] ; then + # ed25519 + openssl req -x509 -nodes -newkey rsa:2048 \ + -keyout $SD/$nick.key \ + -days 3650 -out $SD/$nick.crt || exit 3 + chmod 400 $SD/$nick.key + fi + if [ ! -s $SD/$nick.fp ] ; then + openssl x509 -noout -fingerprint -SHA1 -text \ + < $SD/$nick.crt > $SD/$nick.fp || exit 4 + fi + if [ ! -s $SD/$nick.pem ] ; then + cat $SD/$nick.crt $SD/$nick.key > $SD/$nick.pem + chmod 400 $SD/$nick.pem || exit 5 + fi + ls -l -s $SD/$nick.pem fi +curl -vvvvv --cacert /etc/ssl/cacert-testforge.pem \ + --cert ~/.config/ssl/$HOST/SyniTox.pem \ + https://$HOST:$IRCS_PORT \ + 2>&1| grep "SSL connection using TLSv1.$TLS" + [ $? -gt 0 ] && WARN curl not OK + declare -a RARGS RARGS=( --log_level 10 @@ -32,13 +70,12 @@ RARGS+=( ) declare -a LARGS LARGS=( - --irc_host irc.oftc.net - --irc_port 7000 + --irc_host $HOST + --irc_port $IRC_PORT --irc_ssl "" --irc_ident SyniTox --irc_name SyniTox --irc_nick SyniTox - --irc_pass password ) DBUG $? @@ -50,13 +87,13 @@ fi CIPHER_DOWNGRADE_OVER_TOR=" -Nmap scan report for irc.oftc.net (130.239.18.116) +Nmap scan report for $HOST (130.239.18.116) Host is up (0.26s latency). -Other addresses for irc.oftc.net (not scanned): (null) +Other addresses for $HOST (not scanned): (null) rDNS record for 130.239.18.116: solenoid.acc.umu.se PORT STATE SERVICE -6697/tcp open ircs-u +$IRCS_PORT/tcp open ircs-u | ssl-enum-ciphers: | TLSv1.0: | ciphers: @@ -67,44 +104,44 @@ PORT STATE SERVICE |_ least strength: A " # I know that site does v1.3 3 ciphers -if [ $# -eq 0 -o "$1" = 2 ] ; then - nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p 6697 irc.oftc.net - - # oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion - # irc.oftc.net - LARGS=( - --irc_host irc.oftc.net - --irc_port 6697 +LARGS=( + --irc_host $HOST + --irc_port $IRCS_PORT --irc_ssl tlsv1.$TLS --irc_ident SyniTox --irc_name SyniTox --irc_nick SyniTox --irc_pass password - --irc_pem $HOME/.config/ssl/irc.oftc.net/SyniTox.pem + --irc_pem $HOME/.config/ssl/$HOST/SyniTox.pem # E178E7B9BD9E540278118193AD2C84DEF9B35E85 - --irc_fp $HOME/.config/ssl/irc.oftc.net/SyniTox.fp - --irc_cadir '/etc/ssl/certs' - --irc_cafile /etc/ssl/cacert.pem + --irc_fp $HOME/.config/ssl/$HOST/SyniTox.fp + --irc_cafile /usr/local/etc/ssl/cacert-testforge.pem ) + +if [ $# -eq 0 -o "$1" = 2 ] ; then + INFO SSL v1.$TLS + python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" "$@" DBUG $? fi -if [ $# -eq 0 -o "$1" = 2 ] ; then - INFO SSL - python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" "$@" -fi - -ip=oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion +ip=$ONION if [ $# -eq 0 -o "$1" = 3 ] ; then - nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p 6697 $ip - INFO Onion + nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p $IRCS_PORT $ip + INFO Onion v1.$TLS python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" "$@" DBUG $? fi -ip=`tor-resolve -4 $ip` +ip=`tor-resolve -4 $ONION` if [ $? -eq 0 -a -n "$ip" ] && [ $# -eq 0 -o "$1" = 4 ] ; then - nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p 6697 $ip + curl -vvvvv --cacert /etc/ssl/cacert-testforge.pem \ + --cert ~/.config/ssl/$HOST/SyniTox.pem \ + --connect-to $ip:$IRCS_PORT \ + https://$HOST:$IRCS_PORT \ + 2>&1| grep "SSL connection using TLSv1.$TLS" + + [ $? -gt 0 ] && WARN curl not OK + nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p $IRCS_PORT $ip INFO IP $ip python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" "$@" DBUG $? From f30a1038adcb0c9eaf609d8a0beadf04488b9a0d Mon Sep 17 00:00:00 2001 From: emdee Date: Sun, 6 Nov 2022 03:57:13 +0000 Subject: [PATCH 09/10] Works --- tox-irc-sync.py | 178 ++++++++++++++++++------------ tox-irc-sync_test.bash | 239 ++++++++++++++++++++++++++++++----------- 2 files changed, 286 insertions(+), 131 deletions(-) diff --git a/tox-irc-sync.py b/tox-irc-sync.py index c3431cc..bfc629f 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -42,6 +42,7 @@ LOG = logging.getLogger('app.'+'ts') class SyniToxError(BaseException): pass NAME = 'SyniTox' +sMSG = 'MSG' SSL_TOR_RANGE = '172.' # possible CA locations picks the first one lCAs = [# debian and gentoo @@ -237,23 +238,17 @@ class SyniTox(Tox): context = SSL.Context(SSL.TLS_CLIENT_METHOD) # TLSv1_2_METHOD # SSL.OP_NO_TLSv1_1 is allowed context.set_options(SSL.OP_NO_SSLv2|SSL.OP_NO_SSLv3|SSL.OP_NO_TLSv1) - # this maybe necessary even for a 1.3 site to get the handshake - # in pyOpenSSL - or was it a protocol downgrade attack? -#? context.set_cipher_list("DEFAULT:SECLEVEL=1") - # im getting SSL error: ([('SSL routines', 'tls_construct_client_hello', 'no protocols available')],) - # if I use tlsv1.3 or tlsv1.2 without this on a tlsv1.3 capacble site - if self._oArgs.irc_pem: - key = self._oArgs.irc_pem + if self._oArgs.irc_crt and self._oArgs.irc_key: assert os.path.exists(key), key val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT - LOG.info('Using keyfile: %s' % self._oArgs.irc_pem) - if True: - # key = self._oArgs.irc_pem.replace('.pem', '.crt') + LOG.info('Using keyfile: %s' % key) + if True: # required! + key = self._oArgs.irc_crt assert os.path.exists(key), key context.use_certificate_file(key, filetype=SSL.FILETYPE_PEM) - if True: - # key = self._oArgs.irc_pem.replace('.pem', '.key') + if True: # required! + key = self._oArgs.irc_key assert os.path.exists(key), key context.use_privatekey_file(key, filetype=SSL.FILETYPE_PEM) #? load_client_ca @@ -261,10 +256,10 @@ class SyniTox(Tox): # where in the SSL handshake the function was called, and # the return code from a internal function call print(f"iLine={iLine}, iRet={iRet}") - # context.set_info_callback(SSL_hands_cb) + context.set_info_callback(SSL_hands_cb) def keylog_callback(oConn,s): print(s) - context.set_keylog_callback(keylog_callback) + # context.set_keylog_callback(keylog_callback) else: val = SSL.VERIFY_PEER context.set_verify(val, ssl_verify_cb(HOST, override)) @@ -305,7 +300,7 @@ class SyniTox(Tox): lNodes = ts.generate_nodes(oArgs=self._oArgs, ipv='ipv4', udp_not_tcp=True) - self._settings['current_nodes_udp'] = ts.sDNSClean(lNodes) + self._settings['current_nodes_udp'] = ts.lDNSClean(lNodes) if not lNodes: LOG.warn('empty generate_nodes udp') else: @@ -314,7 +309,7 @@ class SyniTox(Tox): lNodes = ts.generate_nodes(oArgs=self._oArgs, ipv='ipv4', udp_not_tcp=False) - self._settings['current_nodes_tcp'] = ts.sDNSClean(lNodes) + self._settings['current_nodes_tcp'] = ts.lDNSClean(lNodes) if not lNodes: LOG.warn('empty generate_nodes tcp') else: @@ -500,25 +495,29 @@ class SyniTox(Tox): def diagnose_ciphers(self, irc): cipher_name = irc.get_cipher_name() - LOG.info(f"cipher_name={irc.get_cipher_name()}") - LOG.debug(f"get_cipher_list={irc.get_cipher_list()}") + LOG.info(f"diagnose_ciphers cipher_name={irc.get_cipher_name()}") + LOG.debug(f"diagnose_ciphers get_cipher_list={irc.get_cipher_list()}") cipher_list=irc.get_cipher_list() for ci in lOPENSSL_13_CIPHERS: - if ci in cipher_list: LOG.info(f"server supports v1.3 cipher {ci}") + if ci in cipher_list: LOG.debug(f"server supports v1.3 cipher {ci}") + for cert in irc.get_peer_cert_chain(): + # x509 objects - just want the /CN + LOG.debug(f"{cert.get_subject().CN} {cert.get_issuer()}") + cipher_name = irc.get_cipher_name() - LOG.info(f"cipher_name={irc.get_cipher_name()}") if self._oArgs.irc_ssl == 'tlsv1.2': - assert cipher_name in lOPENSSL_12_CIPHERS, cipher_name + assert cipher_name in lOPENSSL_12_CIPHERS or \ + cipher_name in lOPENSSL_13_CIPHERS, cipher_name elif self._oArgs.irc_ssl == 'tlsv1.3': assert cipher_name in lOPENSSL_13_CIPHERS, cipher_name - for cert in irc.get_peer_cert_chain(): - # x509 objects - just want the /CN - LOG.debug(f"{cert.get_subject()} {cert.get_issuer()}") - assert irc.get_protocol_version_name().lower() == \ - self._oArgs.irc_ssl, \ - irc.get_protocol_version_name().lower() - + got = irc.get_protocol_version_name().lower() + if got > self._oArgs.irc_ssl: + LOG.debug(f"Got: {irc.get_protocol_version_name().lower()} asked for {self._oArgs.irc_ssl}") + elif got < self._oArgs.irc_ssl: + LOG.warn(f"Got: {irc.get_protocol_version_name().lower()} asked for {self._oArgs.irc_ssl}") + LOG.info(f"diagnose_ciphers {str(irc.get_state_string(), 'UTF-8')}") + def irc_init(self): global iSocks5Error @@ -562,7 +561,6 @@ class SyniTox(Tox): irc.set_tlsext_host_name(None) else: irc.set_tlsext_host_name(bytes(self._oArgs.irc_host, 'UTF-8')) -#? irc.set_connect_state() while True: try: irc.do_handshake() @@ -579,21 +577,24 @@ class SyniTox(Tox): irc.connect((ip, self._oArgs.irc_port)) LOG.info(f"IRC SSL={self._oArgs.irc_ssl} connected ") - except wrapper_tests.socks.Socks5Error as e: + except (wrapper_tests.socks.GeneralProxyError, wrapper_tests.socks.Socks5Error) as e: iSocks5Error += 1 if iSocks5Error >= iSocks5ErrorMax: raise SyniToxError(f"{e.args}") - if len(e.args[0]) == 2 and e.args[0][0] == 2: - LOG.warn(f"Socks5Error: do you have Tor SafeSocks set? {e.args[0]}") - elif len(e.args[0]) == 2 and e.args[0][0] == 5: - # (5, 'Connection refused') - LOG.warn(f"Socks5Error: do you have Tor running? {e.args[0]}") - raise SyniToxError(f"{e.args}") - elif len(e.args[0]) == 2 and e.args[0][0] in [1, 6]: - # (6, 'TTL expired'), 1, ('general SOCKS server failure') - # Missing mapping for virtual address '172.17.140.117'. Refusing. - LOG.warn(f"Socks5Error: {e.args[0]}") - return + if len(e.args[0]) == 2: + if e.args[0][0] == 2: + LOG.warn(f"Socks5Error: do you have Tor SafeSocks set? {e.args[0]}") + elif e.args[0][0] == 5: + # (5, 'Connection refused') + LOG.warn(f"Socks5Error: do you have Tor running? {e.args[0]}") + raise SyniToxError(f"{e.args}") + elif e.args[0][0] in [1, 6, 0]: + # (0, "connection closed unexpectedly") + # (6, 'TTL expired'), + # 1, ('general SOCKS server failure') + # Missing mapping for virtual address '172.17.140.117'. Refusing. + LOG.warn(f"Socks5Error: {e.args[0]}") + return else: LOG.error(f"Socks5Error: {e.args}") raise SyniToxError(f"{e.args}") @@ -618,12 +619,16 @@ class SyniTox(Tox): return self.irc = irc - if not self._oArgs.irc_ssl: - self.irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) - self.irc.send(bytes('USER %s %s bla :%s\r\n' % ( - self._oArgs.irc_ident, - self._oArgs.irc_host, - self._oArgs.irc_name), 'UTF-8')) + self.irc.send(bytes('CAP ' + 'LS' + '\r\n', 'UTF-8' )) + self.irc.send(bytes('CAP ' + 'REQ :multi-prefix' + '\r\n', 'UTF-8')) + self.irc.send(bytes('CAP ' + 'END' + '\r\n', 'UTF-8' )) + # withh or without self._oArgs.irc_pem: + LOG.info("Sent CAP sending NICK and USER") + self.irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) + self.irc.send(bytes('USER %s %s bla :%s\r\n' % ( + self._oArgs.irc_ident, + self._oArgs.irc_host, + self._oArgs.irc_name), 'UTF-8')) # OSError: [Errno 9] Bad file descriptor @@ -643,7 +648,7 @@ class SyniTox(Tox): self.test_net() lNodes = self._settings['current_nodes_tcp'] shuffle(lNodes) - LOG.debug(f'TCP bootstapping 6') + LOG.info(f'TCP bootstapping 6') ts.bootstrap_tcp(lNodes[:6], [self]) def get_all_groups(self): @@ -714,8 +719,8 @@ class SyniTox(Tox): if b'NOTICE AUTH' in lines[0]: for line in lines[:99]: if b'NOTICE AUTH' not in line: return - line = str(line, 'UTF-8').strip() - print(line) + lines = str(line, 'UTF-8').strip().split() + print(' '.join(lines[1:])) else: for line in lines[:5]: line = str(line, 'UTF-8').strip().lower() @@ -740,44 +745,68 @@ class SyniTox(Tox): l = line.rstrip().split() if len(l) < 2: print(line) - elif l[1] not in ['372']: + elif l[1] in ['PING']: + print(line) + elif l[1] in ['372']: + LOG.info('MOTD') + elif l[1] not in ['372', '353']: i = line.find(' ') print(line[i+1:]) - else: - LOG.info('MOTD') + rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % self._oArgs.irc_chan, line, re.S) - if rx: - self.relay_message(rx) + if l[0] == 'QUIT': + LOG.info('QUIT') + return + if len(l) == 1: + self.irc_send('PING %s\r\n' % '#tor') elif l[0] == 'PING': self.irc_send('PONG %s\r\n' % l[1]) + elif rx: + self.relay_message(rx) elif len(l) < 2: pass elif l[1] in ['461', '431']: pass - elif l[1] in ['433', '462', '477']: + elif l[1] in ['433']: + # maybe should be an outright fail if self._oArgs.irc_ssl: LOG.warn("Maybe the certificate was not received") + #? raise SyniToxError(line) + # sometimes but not always: + # 433 * SyniTox :Nickname is already in use. + # app.ts ERROR SSL error: (32, 'EPIPE') + # or instead + # 451 * :Register first. + # error :closing link: 185.38.175.131 (registration timed out) + # or instead: just + # app.ts ERROR SSL error: (32, 'EPIPE') + pass + elif l[1] in ['451', '462', '477']: + if self._oArgs.irc_crt and self._oArgs.irc_key: + LOG.warn("Maybe the certificate was not received") raise SyniToxError(line) elif l[1] in ['376']: # :End of /MOTD command - if self._oArgs.irc_ssl != '': + if self._oArgs.irc_crt and self._oArgs.irc_key: pass elif email == '' and pwd: - LOG.info(bytes('PRIVMSG NickServ IDENTIFY %s %s\r\n' + LOG.info(bytes(sMSG+' NickServ IDENTIFY %s %s\r\n' % (nick, pwd,), 'UTF-8')) - self.irc.send(bytes('PRIVMSG NickServ IDENTIFY %s %s\r\n' - % (nick, pwd,), 'UTF-8')) + self.irc.send(bytes(sMSG+' NickServ IDENTIFY %s %s\r\n' + % (pwd,nick, ), 'UTF-8')) elif email != '' and pwd: - LOG.info(bytes('PRIVMSG NickServ REGISTER %s %s\r\n' + LOG.info(bytes(sMSG+' NickServ REGISTER %s %s\r\n' % (pwd, email,), 'UTF-8')) - self.irc.send(bytes('PRIVMSG NickServ REGISTER %s %s\r\n' + self.irc.send(bytes(sMSG+' NickServ REGISTER %s %s\r\n' % (pwd, email,), 'UTF-8')) else: LOG.error("you must provide a password to register") raise RuntimeError("you must provide a password to register") try: - self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) + self.irc.send(bytes(sMSG+' NickServ set cloak on\r\n', 'UTF-8')) + if self._oArgs.irc_chan: + self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) except BrokenPipeError: raise SyniToxError('BrokenPipeError') @@ -868,7 +897,7 @@ class SyniTox(Tox): dht_conneted = self.self_get_connection_status() if not dht_conneted: self.dht_init() - LOG.info(f'Not DHT connected {iCount} iterating {10 + iDelay} seconds') + LOG.info(f'Not DHT connected {iCount} iterating {iDelay} seconds') iDelay = iDelay + iDelay // 10 self.do(iDelay) #drop through @@ -930,7 +959,7 @@ class SyniTox(Tox): iDelay = 10 self.irc_readlines() - + self.do(iDelay) return 0 def quit(self): @@ -979,7 +1008,7 @@ class SyniTox(Tox): if message.startswith('>'): message = '\x0309%s\x03' % message - self.irc_send(b'PRIVMSG %s :[%s]: %s\r\n' % + self.irc_send(bsMSG+' %s :[%s]: %s\r\n' % (self._oArgs.irc_chan, name, message)) if message.startswith('^'): self.handle_command(message) @@ -991,8 +1020,8 @@ class SyniTox(Tox): print('TOX> %s: %s' % (name, action)) if action.startswith('>'): action = '\x0309%s\x03' % action - self.irc_send('PRIVMSG %s :\x01ACTION [%s]: %s\x01\r\n' % - (self._oArgs.irc_chan, name, action)) + self.irc_send(bytes(sMSG+' %s :\x01ACTION [%s]: %s\x01\r\n' % + (self._oArgs.irc_chan, name, action), 'UTF-8')) def on_friend_request(self, pk, message): LOG.info('Friend request from %s: %s' % (pk, message)) @@ -1014,7 +1043,7 @@ class SyniTox(Tox): def send_both(self, content): type_ = TOX_MESSAGE_TYPE['NORMAL'] self.ensure_exe(self.group_send_message, self.sGROUP_BOT_NUM, type_, content) - self.irc_send('PRIVMSG %s :%s\r\n' % (self._oArgs.irc_chan, content)) + self.irc_send(bytes(sMSG+' %s :%s\r\n' % (self._oArgs.irc_chan, content), 'UTF-8')) def handle_command(self, cmd): cmd = cmd[1:] @@ -1155,8 +1184,10 @@ def oArgparse(lArgv): parser.add_argument('--irc_cadir', type=str, help="Certificate Authority directory", default=CAcs[0]) - parser.add_argument('--irc_pem', type=str, default='', - help="Certificate and key as pem; use openssl req -x509 -nodes -newkey rsa:2048") + parser.add_argument('--irc_crt', type=str, default='', + help="Certificate as pem; use openssl req -x509 -nodes -newkey rsa:2048") + parser.add_argument('--irc_key', type=str, default='', + help="Key as pem; use openssl req -x509 -nodes -newkey rsa:2048") parser.add_argument('--irc_fp', type=str, default='', help="fingerprint of the pem added with CERT ADD; use openssl x509 -noout -fingerprint -SHA1 -text") parser.add_argument('--irc_nick', type=str, default='', @@ -1210,6 +1241,9 @@ def main(lArgs=None): if lArgs is None: lArgs = [] global oTOX_OARGS oTOX_OARGS = oArgparse(lArgs) + + ts.clean_booleans(oTOX_OARGS) + assert oTOX_OARGS.irc_host or oTOX_OARGS.irc_connect if not oTOX_OARGS.irc_connect: oTOX_OARGS.irc_connect = oTOX_OARGS.irc_host @@ -1217,8 +1251,10 @@ def main(lArgs=None): assert os.path.isdir(oTOX_OARGS.irc_cadir) if oTOX_OARGS.irc_cafile: assert os.path.isfile(oTOX_OARGS.irc_cafile) + global oTOX_OPTIONS oTOX_OPTIONS = oToxygenToxOptions(oTOX_OARGS) + ts.vSetupLogging(oTOX_OARGS) # ts.setup_logging(oArgs) diff --git a/tox-irc-sync_test.bash b/tox-irc-sync_test.bash index c35192a..0683350 100644 --- a/tox-irc-sync_test.bash +++ b/tox-irc-sync_test.bash @@ -5,6 +5,15 @@ #export TOXCORE_LIBS=/mnt/linuxPen19/var/local/src/c-toxcore/_build export TOXCORE_LIBS=/mnt/o/var/local/src/tox_profile/libs export PYTHONPATH=/mnt/o/var/local/src/toxygen_wrapper.git/ +export https_proxy= +export http_proxy= +SOCKS_HOST=127.0.0.1 +SOCKS_PORT=9050 + +NMAP_ARGS="-Pn --script ssl-enum-ciphers --proxies socks4://${SOCKS_HOST}:$SOCKS_PORT --reason" +CURL_ARGS="-vvvvv --cacert /etc/ssl/cacert-testforge.pem" +CURL_ARGS="$CURL_ARGS -x socks5h://${SOCKS_HOST}:$SOCKS_PORT" +CURL_ARGS="$CURL_ARGS --interface lo --dns-interface lo" [ -f /usr/local/bin/usr_local_tput.bash ] && \ . /usr/local/bin/usr_local_tput.bash || { @@ -14,59 +23,120 @@ export PYTHONPATH=/mnt/o/var/local/src/toxygen_wrapper.git/ ERROR() { echo ERROR $* ; } } +if true; then HOST=irc.oftc.net IRC_PORT=6667 IRCS_PORT=6697 ONION=oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion +NICK=SyniTox +TLS=3 +PEM=$HOME/.config/ssl/$HOST/SyniTox.pem +CRT=$HOME/.config/ssl/$HOST/SyniTox.crt +KEY=$HOME/.config/ssl/$HOST/SyniTox.key +FP=$HOME/.config/ssl/$HOST/SyniTox.fp +else +HOST=libera.chat +IRC_PORT= +IRCS_PORT=6697 +ONION=libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion +NICK=SyniTox +PEM=$HOME/.config/ssl/$HOST/SyniTox.pem +KEY=$HOME/.config/ssl/$HOST/SyniTox.key +CRT=$HOME/.config/ssl/$HOST/SyniTox.crt +FP=$HOME/.config/ssl/$HOST/SyniTox.fp +TLS=3 +fi -TLS=0 +function check_nmap() { + local retval=$1 + local hfile=$2 + local tag=$3 + INFO $retval $hfile $tag + if ! grep /tcp $hfile ; then + ERROR check_nmap no /tcp in $hfile + return 1 + # whats filtered? + elif grep '/tcp *filtered' $hfile ; then + WARN check_nmap filtered $hfile + return 2 + # whats filtered? + elif grep '/tcp *open' $hfile ; then + return 0 + fi + return 0 +} + +function check_curl() { + local retval=$1 + local hfile=$2 + local tag=$3 + + # curl: (1) Received HTTP/0.9 when not allowed + if grep "SSL_ERROR_SYSCALL" $hfile ; then + ERROR curl $tag SSL_ERROR_SYSCALL $hfile + return 2 + elif ! grep "SSL connection using TLSv1" $hfile ; then + WARN check_curl curl $tag no ciphers $hfile + elif ! grep "SSL connection using TLSv1.[3$TLS]" $hfile ; then + WARN check_curl curl $tag no TLS connection in $hfile + elif [ $TLS -eq 3 ] && grep "SSL connection using TLSv1.[2]" $hfile ; then + WARN check_curl protocol downgrade attack '?' no TLSv1.3 ciphers from $HOST + elif [ $retval -gt 1 ] ; then + grep "$IRCS_PORT/" $hfile + WARN check_curl curl $tag not OK $retval $hfile + else + INFO curl $tag OK $hfile + return 0 + fi + return 1 +} a=`openssl ciphers -s -v|grep -c v1.3` if [ "$a" -lt 3 ] ; then - WARN no SSSL TLSv1.3 ciphers available to the client. - TLS=2 -elif nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p $IRCS_PORT $HOST | grep -q 'TLSv1.3:' ; then - TLS=3 -else + WARN no SSL TLSv1.3 ciphers available to the client. TLS=2 fi -TLS=3 +[ $TLS = 2 ] && CURL_ARGS="$CURL_ARGS --tlsv1.2" +[ $TLS = 3 ] && CURL_ARGS="$CURL_ARGS --tlsv1.3" +NICK=emdee if [ "$TLS" -ne 0 ] ; then SD=$HOME/.config/ssl/$HOST [ -d $SD ] || mkdir -p $SD || exit 2 - if [ ! -s $SD/$nick.key ] ; then + if [ ! -s $SD/$NICK.key ] ; then # ed25519 openssl req -x509 -nodes -newkey rsa:2048 \ - -keyout $SD/$nick.key \ - -days 3650 -out $SD/$nick.crt || exit 3 - chmod 400 $SD/$nick.key + -keyout $SD/$NICK.key \ + -days 3650 -out $SD/$NICK.crt || exit 3 + chmod 400 $SD/$NICK.key fi - if [ ! -s $SD/$nick.fp ] ; then + if [ ! -s $SD/$NICK.fp ] ; then openssl x509 -noout -fingerprint -SHA1 -text \ - < $SD/$nick.crt > $SD/$nick.fp || exit 4 + < $SD/$NICK.crt > $SD/$NICK.fp || exit 4 fi - if [ ! -s $SD/$nick.pem ] ; then - cat $SD/$nick.crt $SD/$nick.key > $SD/$nick.pem - chmod 400 $SD/$nick.pem || exit 5 + if [ ! -s $SD/$NICK.pem ] ; then + cat $SD/$NICK.crt $SD/$NICK.key > $SD/$NICK.pem + chmod 400 $SD/$NICK.pem || exit 5 fi - ls -l -s $SD/$nick.pem + ls -l -s $SD/$NICK.pem fi +exit 0 -curl -vvvvv --cacert /etc/ssl/cacert-testforge.pem \ - --cert ~/.config/ssl/$HOST/SyniTox.pem \ - https://$HOST:$IRCS_PORT \ - 2>&1| grep "SSL connection using TLSv1.$TLS" - [ $? -gt 0 ] && WARN curl not OK - declare -a RARGS -RARGS=( - --log_level 10 -) +if [ "$DEBUG" = 1 ] ; then + RARGS=( + --log_level 10 + ) +else + RARGS=( + --log_level 20 + ) +fi [ -n "$socks_proxy" ] && \ -RARGS+=( + RARGS+=( --proxy_type 2 --proxy_port 9050 - --proxy_host 127.0.0.1 + --proxy_host ${SOCKS_HOST} + --trace_enabled True ) declare -a LARGS LARGS=( @@ -75,17 +145,32 @@ LARGS=( --irc_ssl "" --irc_ident SyniTox --irc_name SyniTox - --irc_nick SyniTox + --irc_nick $NICK ) -DBUG $? -if [ $# -eq 0 -o "$1" = 1 ] ; then +if [ $# -eq 0 -o "$1" = 1 ] && [ -n "$IRC_PORT" ] ; then INFO No SSL - python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" "$@" + python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" DBUG $? fi -CIPHER_DOWNGRADE_OVER_TOR=" +CIPHER_DOWNGRADE_OVER_TOR_LIBERA="Other addresses for libera.chat (not scanned): (null) +rDNS record for 130.239.18.116: solenoid.acc.umu.se + +PORT STATE SERVICE +6697/tcp open ircs-u +| ssl-enum-ciphers: +| TLSv1.0: +| ciphers: +| TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A +| compressors: +| cipher preference: indeterminate +| cipher preference error: Too few ciphers supported +|_ least strength: A +' +" + +CIPHER_DOWNGRADE_OVER_TOR_OFTC=" Nmap scan report for $HOST (130.239.18.116) Host is up (0.26s latency). @@ -112,37 +197,71 @@ LARGS=( --irc_name SyniTox --irc_nick SyniTox --irc_pass password - --irc_pem $HOME/.config/ssl/$HOST/SyniTox.pem + --irc_crt "$CRT" + --irc_key "$KEY" # E178E7B9BD9E540278118193AD2C84DEF9B35E85 - --irc_fp $HOME/.config/ssl/$HOST/SyniTox.fp + --irc_fp "$FP" --irc_cafile /usr/local/etc/ssl/cacert-testforge.pem ) -if [ $# -eq 0 -o "$1" = 2 ] ; then - INFO SSL v1.$TLS - python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" "$@" - DBUG $? -fi - -ip=$ONION -if [ $# -eq 0 -o "$1" = 3 ] ; then - nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p $IRCS_PORT $ip - INFO Onion v1.$TLS - python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" "$@" - DBUG $? -fi - ip=`tor-resolve -4 $ONION` -if [ $? -eq 0 -a -n "$ip" ] && [ $# -eq 0 -o "$1" = 4 ] ; then - curl -vvvvv --cacert /etc/ssl/cacert-testforge.pem \ - --cert ~/.config/ssl/$HOST/SyniTox.pem \ - --connect-to $ip:$IRCS_PORT \ - https://$HOST:$IRCS_PORT \ - 2>&1| grep "SSL connection using TLSv1.$TLS" - - [ $? -gt 0 ] && WARN curl not OK - nmap --script ssl-enum-ciphers --proxies socks4://127.0.0.1:9050 -p $IRCS_PORT $ip - INFO IP $ip - python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" "$@" +if [ -n "$ip" ] ; then + curl $CURL_ARGS \ + --connect-to $ip:$IRCS_PORT \ + https://$HOST:$IRCS_PORT \ + > /tmp/TIS$$.curl 2>&1 + check_curl $? /tmp/TIS$$.curl "" +else + ERROR tor-resolve failed + exit 6 +fi + +if [ $# -eq 0 -o "$1" = 2 -a $HOST = libera.chat ] ; then + ERROR $HOST rejects tor +elif [ $# -eq 0 -o "$1" = 2 ] ; then + INFO SSL v1.$TLS + python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" + DBUG $? +fi + +if [ -n "$ip" ] ; then + [ -n "$PEM" -a -f "$PEM" ] || { ERROR NO $PEM ; exit 7 ; } + ls -l $PEM || exit 7 + INFO curl $CURL_ARGS \ + --cert-type PEM \ + --cert $PEM \ + --connect-to $ip:$IRCS_PORT \ + https://$HOST:$IRCS_PORT + curl $CURL_ARGS \ + --cert-type PEM \ + --cert $PEM \ + --connect-to $ip:$IRCS_PORT \ + https://$HOST:$IRCS_PORT \ + > /tmp/TIS$$.cert 2>&1 + check_curl $? /tmp/TIS$$.cert "--connect-to" +else + ERROR tor-resolve failed + exit 8 +fi + +if [ $# -eq 0 -o "$1" = 3 ] ; then + [ -n "$PEM" -a -f "$PEM" ] || { ERROR NO $PEM ; exit 7 ; } + + nmap $NMAP_ARGS -p $IRCS_PORT $ip > /tmp/TIS$$.nmap 2>&1 + check_nmap $? /tmp/TIS$$.nmap $1 + + INFO Onion v1.$TLS + python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ONION "${RARGS[@]}" + DBUG $? +fi + +if [ $? -eq 0 ] && [ $# -eq 0 -o "$1" = 4 ] ; then + [ -n "$PEM" -a -f "$PEM" ] || { ERROR NO $PEM ; exit 7 ; } + + nmap $NMAP_ARGS -p $IRCS_PORT $ip > /tmp/TIS$$.nmap 2>&1 + check_nmap $? /tmp/TIS$$.nmap $1 + + INFO Onion v1.$TLS IP $ip + python3 tox-irc-sync.py "${LARGS[@]}" --irc_connect $ip "${RARGS[@]}" DBUG $? fi From f83d1c97b9115d87ba39b67fa4404ccab923c7a9 Mon Sep 17 00:00:00 2001 From: emdee Date: Thu, 17 Nov 2022 12:08:38 +0000 Subject: [PATCH 10/10] pep8 --- tox-irc-sync.py | 309 +++++++++++++++++++++-------------------- tox-irc-sync_test.bash | 1 - 2 files changed, 158 insertions(+), 152 deletions(-) diff --git a/tox-irc-sync.py b/tox-irc-sync.py index bfc629f..a41e65d 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -1,48 +1,51 @@ # -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- -import sys -import os -import socket -import select -import re -import pickle -import logging import ctypes +import logging +import os +import pickle +import re +import select +import socket +import sys import traceback - -from time import sleep -from threading import Thread -from random import shuffle from errno import errorcode +from random import shuffle +from time import sleep + from OpenSSL import SSL -import warnings -warnings.filterwarnings('ignore') - import wrapper +import wrapper.toxcore_enums_and_consts as enums import wrapper_tests from wrapper.tox import Tox from wrapper.toxav import ToxAV -import wrapper.toxcore_enums_and_consts as enums -from wrapper.toxcore_enums_and_consts import \ - TOX_CONNECTION, TOX_USER_STATUS, TOX_MESSAGE_TYPE, \ - TOX_SECRET_KEY_SIZE, TOX_FILE_CONTROL, TOX_ADDRESS_SIZE, \ - TOX_GROUP_PRIVACY_STATE, TOX_GROUP_ROLE +from wrapper.toxcore_enums_and_consts import (TOX_ADDRESS_SIZE, TOX_CONNECTION, + TOX_FILE_CONTROL, + TOX_GROUP_PRIVACY_STATE, + TOX_GROUP_ROLE, TOX_MESSAGE_TYPE, + TOX_SECRET_KEY_SIZE, + TOX_USER_STATUS) from wrapper_tests import socks try: import support_testing as ts except ImportError: import wrapper_tests.support_testing as ts -import wrapper.toxencryptsave as tox_encrypt_save +import wrapper.toxencryptsave as tox_encrypt_save global LOG LOG = logging.getLogger('app.'+'ts') +import warnings + +warnings.filterwarnings('ignore') + class SyniToxError(BaseException): pass NAME = 'SyniTox' sMSG = 'MSG' +sMSG = 'PRIVMSG' SSL_TOR_RANGE = '172.' # possible CA locations picks the first one lCAs = [# debian and gentoo @@ -138,10 +141,10 @@ class SyniTox(Tox): opts = oTOX_OPTIONS self._opts = opts - self._oArgs = oArgs + self._oargs = oArgs - # self._oArgs.profile - self.load_profile(self._opts, self._oArgs, self._oArgs.password) + # self._oargs.profile + self.load_profile(self._opts, self._oargs, self._oargs.password) Tox.__init__(self, tox_options=self._opts) self._address = self.self_get_address() @@ -170,7 +173,6 @@ class SyniTox(Tox): def load_profile(self, tox_options, oArgs, password=''): if oArgs.profile and os.path.exists(oArgs.profile): data = open(oArgs.profile, 'rb').read() - else: data = None if data and self.has_password(): @@ -191,20 +193,20 @@ class SyniTox(Tox): data = self.pass_encrypt(data) try: suf = f"{os.getpid()}" - with open(self._oArgs.profile+suf, 'wb') as fl: + with open(self._oargs.profile+suf, 'wb') as fl: fl.write(data) - stat = os.stat(self._oArgs.profile+suf) + stat = os.stat(self._oargs.profile+suf) if hasattr(stat, 'st_blocks'): - assert stat.st_blocks > 0, f"Zero length file {self._oArgs.profile+suf}" - os.rename(self._oArgs.profile+suf, self._oArgs.profile) - LOG.info('Profile saved successfully to' +self._oArgs.profile) + assert stat.st_blocks > 0, f"Zero length file {self._oargs.profile+suf}" + os.rename(self._oargs.profile+suf, self._oargs.profile) + LOG.info('Profile saved successfully to' +self._oargs.profile) except Exception as e: - LOG.warn(f"Profile save failed to {self._oArgs.profile}\n{e}") + LOG.warn(f"Profile save failed to {self._oargs.profile}\n{e}") def start(self): self._tox = self self._toxes = tox_encrypt_save.ToxEncryptSave() - self.self_set_name(self._oArgs.bot_name) + self.self_set_name(self._oargs.bot_name) self.self_set_status_message("Send me a message with the word 'invite'") LOG.info('Our ToxID: %s' % self.self_get_toxid()) @@ -220,17 +222,17 @@ class SyniTox(Tox): try: OP_NO_TLSv1_3 = SSL._lib.SSL_OP_NO_TLSv1_3 except AttributeError: - if self._oArgs.irc_ssl == 'tlsv1.3': + if self._oargs.irc_ssl == 'tlsv1.3': LOG.warning("SSL._lib.SSL_OP_NO_TLSv1_3 is not supported") LOG.warning("Downgrading SSL to tlsv1.2 ") - self._oArgs.irc_ssl = 'tlsv1.2' + self._oargs.irc_ssl = 'tlsv1.2' else: LOG.debug("SSL._lib.SSL_OP_NO_TLSv1_3 is not supported") else: LOG.debug("SSL._lib.SSL_OP_NO_TLSv1_3 is supported") - if self._oArgs.irc_connect.endswith('.onion') or \ - self._oArgs.irc_connect.startswith(SSL_TOR_RANGE): + if self._oargs.irc_connect.endswith('.onion') or \ + self._oargs.irc_connect.startswith(SSL_TOR_RANGE): override = True else: override = False @@ -239,17 +241,17 @@ class SyniTox(Tox): # SSL.OP_NO_TLSv1_1 is allowed context.set_options(SSL.OP_NO_SSLv2|SSL.OP_NO_SSLv3|SSL.OP_NO_TLSv1) - if self._oArgs.irc_crt and self._oArgs.irc_key: - assert os.path.exists(key), key + if self._oargs.irc_crt and self._oargs.irc_key: val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT - LOG.info('Using keyfile: %s' % key) if True: # required! - key = self._oArgs.irc_crt + key = self._oargs.irc_crt assert os.path.exists(key), key + LOG.info('Using keyfile: %s' % key) context.use_certificate_file(key, filetype=SSL.FILETYPE_PEM) if True: # required! - key = self._oArgs.irc_key + key = self._oargs.irc_key assert os.path.exists(key), key + LOG.info('Using keyfile: %s' % key) context.use_privatekey_file(key, filetype=SSL.FILETYPE_PEM) #? load_client_ca def SSL_hands_cb(oConn,iLine,iRet): @@ -264,17 +266,17 @@ class SyniTox(Tox): val = SSL.VERIFY_PEER context.set_verify(val, ssl_verify_cb(HOST, override)) - if self._oArgs.irc_cafile: - # context.load_verify_locations(capath=self._oArgs.irc_ca) - context.load_verify_locations(self._oArgs.irc_cafile, capath=self._oArgs.irc_cadir) - elif self._oArgs.irc_cadir: - context.load_verify_locations(None, capath=self._oArgs.irc_cadir) - if self._oArgs.irc_ssl == 'tlsv1.1': + if self._oargs.irc_cafile: + # context.load_verify_locations(capath=self._oargs.irc_ca) + context.load_verify_locations(self._oargs.irc_cafile, capath=self._oargs.irc_cadir) + elif self._oargs.irc_cadir: + context.load_verify_locations(None, capath=self._oargs.irc_cadir) + if self._oargs.irc_ssl == 'tlsv1.1': context.set_min_proto_version(SSL.TLS1_1_VERSION) - elif self._oArgs.irc_ssl == 'tlsv1.2': + elif self._oargs.irc_ssl == 'tlsv1.2': context.set_cipher_list(bytes(':'.join(['DEFAULT@SECLEVEL=1']+lOPENSSL_12_CIPHERS), 'UTF-8')) context.set_min_proto_version(SSL.TLS1_2_VERSION) - elif self._oArgs.irc_ssl == 'tlsv1.3': + elif self._oargs.irc_ssl == 'tlsv1.3': context.set_cipher_list(bytes(':'.join(['DEFAULT@SECLEVEL=1']+lOPENSSL_13_CIPHERS), 'UTF-8')) context.set_min_proto_version(SSL.TLS1_3_VERSION) self._ssl_context = context @@ -282,7 +284,7 @@ class SyniTox(Tox): return self._ssl_context def bRouted(self): - if self._oArgs.network in ['local']: + if self._oargs.network in ['local']: return True b = ts.bAreWeConnected() if b is None: @@ -295,9 +297,9 @@ class SyniTox(Tox): return b def test_net(self, lElts=None, oThread=None, iMax=4): - LOG.debug("test_net network=" +self._oArgs.network ) + LOG.debug("test_net network=" +self._oargs.network ) # bootstrap - lNodes = ts.generate_nodes(oArgs=self._oArgs, + lNodes = ts.generate_nodes(oArgs=self._oargs, ipv='ipv4', udp_not_tcp=True) self._settings['current_nodes_udp'] = ts.lDNSClean(lNodes) @@ -306,7 +308,7 @@ class SyniTox(Tox): else: LOG.info(f'Called generate_nodes: udp {len(lNodes)}') - lNodes = ts.generate_nodes(oArgs=self._oArgs, + lNodes = ts.generate_nodes(oArgs=self._oargs, ipv='ipv4', udp_not_tcp=False) self._settings['current_nodes_tcp'] = ts.lDNSClean(lNodes) @@ -341,8 +343,8 @@ class SyniTox(Tox): # The code in tests_wrapper need extending and then # wiring up to here. # - if self._oArgs.group_invite: - pk = self._oArgs.group_invite + if self._oargs.group_invite: + pk = self._oargs.group_invite if pk not in self.self_get_friend_list(): friend_number = self.add_friend(pk) else: @@ -351,8 +353,8 @@ class SyniTox(Tox): LOG.info(f"A PK to invite to the group {b}") return True - if self._oArgs.group_moderator: - pk = self._oArgs.group_moderator + if self._oargs.group_moderator: + pk = self._oargs.group_moderator if pk not in self.self_get_friend_list(): friend_number = self.add_friend(pk) else: @@ -364,8 +366,8 @@ class SyniTox(Tox): LOG.info("A PK to invite to the group as moderator {b}") return True - if self._oArgs.group_ignore: - pk = self._oArgs.group_ignore + if self._oargs.group_ignore: + pk = self._oargs.group_ignore if pk not in self.self_get_friend_list(): friend_number = self.add_friend(pk) else: @@ -379,21 +381,18 @@ class SyniTox(Tox): return None def create_group(self): - privacy_state = TOX_GROUP_PRIVACY_STATE[self._oArgs.group_state.upper()] - nick = self._oArgs.group_nick - group_name = self._oArgs.group_name - if not group_name: - group_name = self._oArgs.bot_name +self._oArgs.irc_chan - self._oArgs.group_name = group_name + privacy_state = TOX_GROUP_PRIVACY_STATE[self._oargs.group_state.upper()] + nick = self._oargs.group_nick status = TOX_USER_STATUS['NONE'] + group_name = self._oargs.group_name num = self.group_new(privacy_state, group_name, nick, status) assert num >= 0, num - self.group_set_topic(num, f"{group_name} IRC on {self._oArgs.irc_host}" ) + self.group_set_topic(num, f"{group_name} IRC on {self._oargs.irc_host}" ) # self.tox_group_id = self.group_invite_accept(b'', friendid, nick) chat_id = self.group_get_chat_id(num) - if self._oArgs.profile and os.path.exists(os.path.dirname(self._oArgs.profile)): - f = os.path.splitext(self._oArgs.profile)[0] +'.chatid' + if self._oargs.profile and os.path.exists(os.path.dirname(self._oargs.profile)): + f = os.path.splitext(self._oargs.profile)[0] +'.chatid' open(f, 'rt').write(chat_id) LOG.info(f"Chat Id: {chat_id} written to {f}") else: @@ -401,16 +400,16 @@ class SyniTox(Tox): # dunno if self.self_get_friend_list(): friendid = self.self_get_friend_list()[0] - i = on_group_invite(friendid, b'', 0) + i = self.on_group_invite(friendid, b'', 0) assert i self.tox_group_id = i return num def join_group(self): - password = self._oArgs.group_pass - nick = self._oArgs.group_nick + password = self._oargs.group_pass + nick = self._oargs.group_nick # is the chat_id the pk? - chat_id = self._oArgs.group_chatid + chat_id = self._oargs.group_chatid if not chat_id: return -1 num = self.group_join(chat_id, password, nick, status='') self.sGROUP_BOT_NUM = num @@ -418,8 +417,7 @@ class SyniTox(Tox): return num def init_groups(self): - LOG.debug(f"init_groups proxy={self._oArgs.proxy_type}") - group_name = self._oArgs.bot_name +' Test ' +self._oArgs.irc_chan + LOG.debug(f"init_groups proxy={self._oargs.proxy_type}") if not self.bRouted(): return try: if self.sGROUP_BOT_NUM < 0: @@ -458,7 +456,7 @@ class SyniTox(Tox): self.callback_group_invite(gi_wrapped, 0) def scs_wrapped(iTox, friendid, status, *args): - LOG.debug(f'on_connection_status {friendId} {status}.') + LOG.debug(f'on_connection_status {friendid} {status}.') self.on_connection_status(friendid, status) self.callback_self_connection_status(scs_wrapped) @@ -505,62 +503,59 @@ class SyniTox(Tox): LOG.debug(f"{cert.get_subject().CN} {cert.get_issuer()}") cipher_name = irc.get_cipher_name() - if self._oArgs.irc_ssl == 'tlsv1.2': + if self._oargs.irc_ssl == 'tlsv1.2': assert cipher_name in lOPENSSL_12_CIPHERS or \ cipher_name in lOPENSSL_13_CIPHERS, cipher_name - elif self._oArgs.irc_ssl == 'tlsv1.3': + elif self._oargs.irc_ssl == 'tlsv1.3': assert cipher_name in lOPENSSL_13_CIPHERS, cipher_name got = irc.get_protocol_version_name().lower() - if got > self._oArgs.irc_ssl: - LOG.debug(f"Got: {irc.get_protocol_version_name().lower()} asked for {self._oArgs.irc_ssl}") - elif got < self._oArgs.irc_ssl: - LOG.warn(f"Got: {irc.get_protocol_version_name().lower()} asked for {self._oArgs.irc_ssl}") + if got > self._oargs.irc_ssl: + LOG.debug(f"Got: {irc.get_protocol_version_name().lower()} asked for {self._oargs.irc_ssl}") + elif got < self._oargs.irc_ssl: + LOG.warn(f"Got: {irc.get_protocol_version_name().lower()} asked for {self._oargs.irc_ssl}") LOG.info(f"diagnose_ciphers {str(irc.get_state_string(), 'UTF-8')}") def irc_init(self): global iSocks5Error if not self.bRouted(): return - nick = self._oArgs.irc_nick - realname = self._oArgs.irc_name - ident = self._oArgs.irc_ident - LOG.info(f"irc_init proxy={self._oArgs.proxy_type} SSL={self._oArgs.irc_ssl}") + nick = self._oargs.irc_nick + realname = self._oargs.irc_name + ident = self._oargs.irc_ident + LOG.info(f"irc_init proxy={self._oargs.proxy_type} SSL={self._oargs.irc_ssl}") try: - if self._oArgs.proxy_type == 2: + if self._oargs.proxy_type == 2: socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, - self._oArgs.proxy_host, - self._oArgs.proxy_port) + self._oargs.proxy_host, + self._oargs.proxy_port) irc = socks.socksocket() iTIMEOUT = 15 - elif self._oArgs.proxy_type == 1: + elif self._oargs.proxy_type == 1: socks.setdefaultproxy(socks.PROXY_TYPE_HTTP, - self._oArgs.proxy_host, - self._oArgs.proxy_port) + self._oargs.proxy_host, + self._oargs.proxy_port) irc = socks.socksocket() iTIMEOUT = 15 else: irc = socket.socket() iTIMEOUT = 10 try: - ip = ts.sDNSLookup(self._oArgs.irc_connect) + ip = ts.sDNSLookup(self._oargs.irc_connect) except Exception as e: - LOG.warn(f"{self._oArgs.irc_host} errored in resolve {e}") - ip = self._oArgs.irc_connect + LOG.warn(f"{self._oargs.irc_host} errored in resolve {e}") + ip = self._oargs.irc_connect else: if not ip: - LOG.warn(f"{self._oArgs.irc_host} did not resolve.") - ip = self._oArgs.irc_connect + LOG.warn(f"{self._oargs.irc_host} did not resolve.") + ip = self._oargs.irc_connect # https://github.com/pyca/pyopenssl/issues/168 - if self._oArgs.irc_ssl: + if self._oargs.irc_ssl: if not self._ssl_context: - self.start_ssl(self._oArgs.irc_connect) + self.start_ssl(self._oargs.irc_connect) irc = SSL.Connection(self._ssl_context, irc) - irc.connect((ip, self._oArgs.irc_port)) - if ip.endswith('.onion'): - irc.set_tlsext_host_name(None) - else: - irc.set_tlsext_host_name(bytes(self._oArgs.irc_host, 'UTF-8')) + irc.connect((ip, self._oargs.irc_port)) + irc.set_tlsext_host_name(bytes(self._oargs.irc_host, 'UTF-8')) while True: try: irc.do_handshake() @@ -569,15 +564,15 @@ class SyniTox(Tox): if not rd: raise socket.timeout('timeout') continue - except SSL.Error as e: + except SSL.Error as e: # noqa raise break self.diagnose_ciphers(irc) else: - irc.connect((ip, self._oArgs.irc_port)) - LOG.info(f"IRC SSL={self._oArgs.irc_ssl} connected ") + irc.connect((ip, self._oargs.irc_port)) + LOG.info(f"IRC SSL={self._oargs.irc_ssl} connected ") - except (wrapper_tests.socks.GeneralProxyError, wrapper_tests.socks.Socks5Error) as e: + except (wrapper_tests.socks.GeneralProxyError, wrapper_tests.socks.Socks5Error) as e: # noqa iSocks5Error += 1 if iSocks5Error >= iSocks5ErrorMax: raise SyniToxError(f"{e.args}") @@ -622,13 +617,13 @@ class SyniTox(Tox): self.irc.send(bytes('CAP ' + 'LS' + '\r\n', 'UTF-8' )) self.irc.send(bytes('CAP ' + 'REQ :multi-prefix' + '\r\n', 'UTF-8')) self.irc.send(bytes('CAP ' + 'END' + '\r\n', 'UTF-8' )) - # withh or without self._oArgs.irc_pem: + # withh or without self._oargs.irc_pem: LOG.info("Sent CAP sending NICK and USER") self.irc.send(bytes('NICK ' + nick + '\r\n', 'UTF-8' )) self.irc.send(bytes('USER %s %s bla :%s\r\n' % ( - self._oArgs.irc_ident, - self._oArgs.irc_host, - self._oArgs.irc_name), 'UTF-8')) + self._oargs.irc_ident, + self._oargs.irc_host, + self._oargs.irc_name), 'UTF-8')) # OSError: [Errno 9] Bad file descriptor @@ -638,7 +633,7 @@ class SyniTox(Tox): self.test_net() lNodes = self._settings['current_nodes_udp'] shuffle(lNodes) - if self._oArgs.proxy_type == 0: + if self._oargs.proxy_type == 0: ts.bootstrap_udp(lNodes[:6], [self]) else: if self._bRouted is None: @@ -654,7 +649,7 @@ class SyniTox(Tox): def get_all_groups(self): try: group_numbers = range(self._tox.group_get_number_groups()) - except Exception as e: + except Exception as e: # noqa return None groups = map(lambda n: self.get_group_by_number(n), group_numbers) @@ -730,10 +725,10 @@ class SyniTox(Tox): raise SyniToxError(line) def irc_readlines(self): - nick = self._oArgs.irc_nick - pwd = self._oArgs.irc_pass - fp = self._oArgs.irc_fp - email = self._oArgs.irc_email + nick = self._oargs.irc_nick + pwd = self._oargs.irc_pass + fp = self._oargs.irc_fp + email = self._oargs.irc_email self.readbuffer += self.irc.recv(4096) lines = self.readbuffer.split(b'\n') @@ -754,7 +749,7 @@ class SyniTox(Tox): print(line[i+1:]) rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % - self._oArgs.irc_chan, line, re.S) + self._oargs.irc_chan, line, re.S) if l[0] == 'QUIT': LOG.info('QUIT') return @@ -770,7 +765,7 @@ class SyniTox(Tox): pass elif l[1] in ['433']: # maybe should be an outright fail - if self._oArgs.irc_ssl: + if self._oargs.irc_ssl: LOG.warn("Maybe the certificate was not received") #? raise SyniToxError(line) # sometimes but not always: @@ -783,13 +778,14 @@ class SyniTox(Tox): # app.ts ERROR SSL error: (32, 'EPIPE') pass elif l[1] in ['451', '462', '477']: - if self._oArgs.irc_crt and self._oArgs.irc_key: + if self._oargs.irc_crt and self._oargs.irc_key: LOG.warn("Maybe the certificate was not received") raise SyniToxError(line) elif l[1] in ['376']: # :End of /MOTD command - if self._oArgs.irc_crt and self._oArgs.irc_key: - pass + if self._oargs.irc_crt and self._oargs.irc_key: + LOG.info(bytes(sMSG+' NickServ IDENTIFY %s %s\r\n' + % (nick, pwd,), 'UTF-8')) elif email == '' and pwd: LOG.info(bytes(sMSG+' NickServ IDENTIFY %s %s\r\n' % (nick, pwd,), 'UTF-8')) @@ -805,8 +801,8 @@ class SyniTox(Tox): raise RuntimeError("you must provide a password to register") try: self.irc.send(bytes(sMSG+' NickServ set cloak on\r\n', 'UTF-8')) - if self._oArgs.irc_chan: - self.irc.send(bytes('JOIN %s\r\n' % self._oArgs.irc_chan, 'UTF-8')) + if self._oargs.irc_chan: + self.irc.send(bytes('JOIN %s\r\n' % self._oargs.irc_chan, 'UTF-8')) except BrokenPipeError: raise SyniToxError('BrokenPipeError') @@ -869,13 +865,13 @@ class SyniTox(Tox): iCount = 0 iDelay = 10 - nick = self._oArgs.irc_nick - realname = self._oArgs.irc_name - ident = self._oArgs.irc_ident - pwd = self._oArgs.irc_pass - email = self._oArgs.irc_email + nick = self._oargs.irc_nick + realname = self._oargs.irc_name + ident = self._oargs.irc_ident + pwd = self._oargs.irc_pass + email = self._oargs.irc_email LOG.info(f"Looping for Tox and IRC connections") - if iCount < self._oArgs.max_sleep: + if iCount < self._oargs.max_sleep: while True: iCount += 1 # LOG.debug(f"Looping {iCount}") @@ -909,7 +905,7 @@ class SyniTox(Tox): #? self.bid = self.friend_by_public_key(self.sGROUP_BOT_PK) r = self.group_reconnect(self.sGROUP_BOT_NUM) LOG.info(f'Connected to group {r}') - except ctypes.ArgumentError as e: + except ctypes.ArgumentError as e: # noqa self.bid = None if self.bid == None: @@ -933,7 +929,7 @@ class SyniTox(Tox): continue - LOG.info(f'Waiting on IRC to {self._oArgs.irc_host} on {self._oArgs.irc_port}') + LOG.info(f'Waiting on IRC to {self._oargs.irc_host} on {self._oargs.irc_port}') readable = self.spin(20) if not readable or not readable[0]: @@ -993,7 +989,7 @@ class SyniTox(Tox): def on_group_invite(self, friendid, invite_data, user_data): if not self.joined: self.joined = True - nick = self._oArgs.group_nick + nick = self._oargs.group_nick self.tox_group_id = self.group_invite_accept(invite_data, friendid, nick) LOG.info('Joined groupchat.') @@ -1007,9 +1003,8 @@ class SyniTox(Tox): print('TOX> %s: %s' % (name, message)) if message.startswith('>'): message = '\x0309%s\x03' % message - - self.irc_send(bsMSG+' %s :[%s]: %s\r\n' % - (self._oArgs.irc_chan, name, message)) + self.irc_send(sMSG+' %s :[%s]: %s\r\n' % + (self._oargs.irc_chan, name, message)) if message.startswith('^'): self.handle_command(message) @@ -1020,8 +1015,8 @@ class SyniTox(Tox): print('TOX> %s: %s' % (name, action)) if action.startswith('>'): action = '\x0309%s\x03' % action - self.irc_send(bytes(sMSG+' %s :\x01ACTION [%s]: %s\x01\r\n' % - (self._oArgs.irc_chan, name, action), 'UTF-8')) + self.irc_send(bytes(sMSG' %s :\x01ACTION [%s]: %s\x01\r\n' % + (self._oargs.irc_chan, name, action), 'UTF-8')) def on_friend_request(self, pk, message): LOG.info('Friend request from %s: %s' % (pk, message)) @@ -1043,7 +1038,7 @@ class SyniTox(Tox): def send_both(self, content): type_ = TOX_MESSAGE_TYPE['NORMAL'] self.ensure_exe(self.group_send_message, self.sGROUP_BOT_NUM, type_, content) - self.irc_send(bytes(sMSG+' %s :%s\r\n' % (self._oArgs.irc_chan, content), 'UTF-8')) + self.irc_send(bytes(sMSG+' %s :%s\r\n' % (self._oargs.irc_chan, content), 'UTF-8')) def handle_command(self, cmd): cmd = cmd[1:] @@ -1067,13 +1062,13 @@ class SyniTox(Tox): return len(data) > 0 and self._toxes.is_data_encrypted(data) def pass_encrypt(self, data): - return self._toxes.pass_encrypt(data, self._oArgs.password) + return self._toxes.pass_encrypt(data, self._oargs.password) def has_password(self): - return self._oArgs.password + return self._oargs.password def pass_decrypt(self, data): - return self._toxes.pass_decrypt(data, self._oArgs.password) + return self._toxes.pass_decrypt(data, self._oargs.password) def iMain(oArgs, oOpts): @@ -1091,7 +1086,7 @@ def iMain(oArgs, oOpts): except ( SSL.Error, ) as e: LOG.error(f"SSL error: {e.args}") ret = 1 - except (SSL.SysCallError, ) as e: + except (SSL.SysCallError,) as e: # OpenSSL.SSL.SysCallError: (9, 'EBADF') LOG.error(f"SSL error: {e.args}") ret = 1 @@ -1140,6 +1135,25 @@ def oToxygenToxOptions(oArgs): return tox_options +def vInitializeOargs(): + global oTOX_OARGS + assert oTOX_OARGS.irc_host or oTOX_OARGS.irc_connect + if not oTOX_OARGS.irc_connect: + oTOX_OARGS.irc_connect = oTOX_OARGS.irc_host + if oTOX_OARGS.irc_cadir: + assert os.path.isdir(oTOX_OARGS.irc_cadir) + if oTOX_OARGS.irc_cafile: + assert os.path.isfile(oTOX_OARGS.irc_cafile) + if oTOX_OARGS.irc_crt: + assert os.path.isfile(oTOX_OARGS.irc_crt) + assert oTOX_OARGS.irc_key + if oTOX_OARGS.irc_key: + assert os.path.isfile(oTOX_OARGS.irc_key) + assert oTOX_OARGS.irc_crt + if not oTOX_OARGS.group_name: + group_name = oTOX_OARGS.bot_name +oTOX_OARGS.irc_chan + oTOX_OARGS.group_name = group_name + def oArgparse(lArgv): parser = ts.oMainArgparser() parser.add_argument('profile', type=str, nargs='?', default=None, @@ -1179,7 +1193,7 @@ def oArgparse(lArgv): help="TLS version; empty is no SSL", choices=['', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3']) parser.add_argument('--irc_cafile', type=str, - help="Certificate Authority file", + help="Certificate Authority file (in PEM)", default=CAfs[0]) parser.add_argument('--irc_cadir', type=str, help="Certificate Authority directory", @@ -1243,14 +1257,7 @@ def main(lArgs=None): oTOX_OARGS = oArgparse(lArgs) ts.clean_booleans(oTOX_OARGS) - - assert oTOX_OARGS.irc_host or oTOX_OARGS.irc_connect - if not oTOX_OARGS.irc_connect: - oTOX_OARGS.irc_connect = oTOX_OARGS.irc_host - if oTOX_OARGS.irc_cadir: - assert os.path.isdir(oTOX_OARGS.irc_cadir) - if oTOX_OARGS.irc_cafile: - assert os.path.isfile(oTOX_OARGS.irc_cafile) + vInitializeOargs() global oTOX_OPTIONS oTOX_OPTIONS = oToxygenToxOptions(oTOX_OARGS) diff --git a/tox-irc-sync_test.bash b/tox-irc-sync_test.bash index 0683350..b440ea8 100644 --- a/tox-irc-sync_test.bash +++ b/tox-irc-sync_test.bash @@ -119,7 +119,6 @@ if [ "$TLS" -ne 0 ] ; then fi ls -l -s $SD/$NICK.pem fi -exit 0 declare -a RARGS if [ "$DEBUG" = 1 ] ; then