diff --git a/README.md b/README.md index de88c36..f894e0f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,156 @@ -#Tox-Sync +# Tox-Sync + +A bot that sync messages between IRC and Tox NGC group chat. + +## Hard Forked + +Hard forked from +and changed to use the Python wrapping from +. +Just clone that repo and put the resulting directory on your +```PYTHONPATH```. + +## Usage + +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 "#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: +``` +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 \ + profile_that_will_get_the_group_key.tox +``` + +* OFTC has an Onion address: + ```ircs://oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion:6697``` +* Libera has an Onion address: + ```libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion``` + + +## 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. -A bot that sync messages between Freenode IRC #tox-ontopic and Tox group chat. diff --git a/tox-irc-sync.py b/tox-irc-sync.py index 9c386ec..a41e65d 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -1,245 +1,1049 @@ -import sys -import socket -import string -import select -import re +# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- + +import ctypes +import logging +import os import pickle - -from pytox import Tox, ToxAV - +import re +import select +import socket +import sys +import traceback +from errno import errorcode +from random import shuffle from time import sleep -from os.path import exists -from threading import Thread -SERVER = ['54.199.139.199', 33445, '7F9C31FE850E97CEFD4C4591DF93FC757C7C12549DDD55F8EEAECC34FE76C029'] -GROUP_BOT = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5' -PWD = '' +from OpenSSL import SSL -IRC_HOST = 'irc.freenode.net' -IRC_PORT = 6667 -NAME = NICK = IDENT = REALNAME = 'SyncBot' +import wrapper +import wrapper.toxcore_enums_and_consts as enums +import wrapper_tests +from wrapper.tox import Tox +from wrapper.toxav import ToxAV +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 -CHANNEL = '#tox-ontopic' -MEMORY_DB = 'memory.pickle' +try: + import support_testing as ts +except ImportError: + import wrapper_tests.support_testing as ts -class AV(ToxAV): - def __init__(self, core, max_calls): - self.core = self.get_tox() - self.cs = None - self.call_type = self.TypeAudio +import wrapper.toxencryptsave as tox_encrypt_save - def on_invite(self, idx): - self.cs = self.get_peer_csettings(idx, 0) - self.call_type = self.cs['call_type'] +global LOG +LOG = logging.getLogger('app.'+'ts') +import warnings - print('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)))) +warnings.filterwarnings('ignore') - self.answer(idx, self.call_type) - print('Answered, in call...') +class SyniToxError(BaseException): pass - 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) +NAME = 'SyniTox' +sMSG = 'MSG' +sMSG = 'PRIVMSG' +SSL_TOR_RANGE = '172.' +# possible CA locations picks the first one +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 - def on_end(self, idx): - self.kill_transmission() +# 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) - print('Call ended') +# https://wiki.python.org/moin/SSL +def ssl_verify_cb(host, override=False): + assert 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}") - def on_peer_timeout(self, idx): - self.stop_call() + # 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 - def on_audio_data(self, idx, size, data): - sys.stdout.write('.') - sys.stdout.flush() - self.send_audio(idx, size, data) + if x509.get_subject().commonName == host: + return True - def on_video_data(self, idx, width, height, data): - sys.stdout.write('*') - sys.stdout.flush() - self.send_video(idx, width, height, data) + # 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:]: + LOG.warn(f"ssl_verify accepting {x509.get_subject().commonName} for {host}") + return True -bot_toxname = 'SyncBot' + return False -class SyncBot(Tox): - def __init__(self): - if exists('data'): - self.load_from_file('data') + return ssl_verify - 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()) + +class SyniTox(Tox): - self.readbuffer = '' - self.tox_group_id = None + def __init__(self, + oArgs, + oOpts, + GROUP_BOT_PK = '', + sMEMORY_DB = '' + ): - self.irc_init() + 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.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 - if exists(MEMORY_DB): - with open(MEMORY_DB, 'r') as f: + 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._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.tox_group_id = None + self.init_callbacks() + + if os.path.exists(self.sMEMORY_DB): + with open(self.sMEMORY_DB, 'r') as f: self.memory = pickle.load(f) + def start_ssl(self, HOST): + if not self._ssl_context: + 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.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) + + if self._oargs.irc_crt and self._oargs.irc_key: + val = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT + if True: # required! + 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 + 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): + # 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)) + + 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(['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(['DEFAULT@SECLEVEL=1']+lOPENSSL_13_CIPHERS), 'UTF-8')) + context.set_min_proto_version(SSL.TLS1_3_VERSION) + self._ssl_context = context + + return self._ssl_context + + def bRouted(self): + 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'] = ts.lDNSClean(lNodes) + if not lNodes: + LOG.warn('empty generate_nodes udp') + else: + 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'] = ts.lDNSClean(lNodes) + if not lNodes: + LOG.warn('empty generate_nodes tcp') + else: + LOG.info(f'Called generate_nodes: tcp {len(lNodes)}') + + # if oThread and oThread._stop_thread: return + return True + + 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 + + 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") + + # 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 + 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.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 = 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 + # 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']) + return num + + def init_groups(self): + LOG.debug(f"init_groups proxy={self._oargs.proxy_type}") + 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 + + def init_callbacks(self): + return + # 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') + 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, 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') + 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') + 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): + 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 diagnose_ciphers(self, irc): + cipher_name = irc.get_cipher_name() + 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.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() + 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': + 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}") + LOG.info(f"diagnose_ciphers {str(irc.get_state_string(), 'UTF-8')}") + 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)) + 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}") + try: + if self._oargs.proxy_type == 2: + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, + 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_connect) + irc = SSL.Connection(self._ssl_context, irc) + 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() + except SSL.WantReadError: + rd,_,_ = select.select([irc], [], [], irc.gettimeout()) + if not rd: + raise socket.timeout('timeout') + continue + 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 ") - def connect(self): - print('connecting...') - self.bootstrap_from_address(SERVER[0], SERVER[1], SERVER[2]) + except (wrapper_tests.socks.GeneralProxyError, wrapper_tests.socks.Socks5Error) as e: # noqa + iSocks5Error += 1 + if iSocks5Error >= iSocks5ErrorMax: + raise SyniToxError(f"{e.args}") + 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}") + 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 Exception as e: + LOG.warn(f"Error: {e}") + LOG.warn(traceback.format_exc()) + return - def ensure_exe(self, func, args): + self.irc = irc + 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 + + 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_udp(lNodes[:6], [self]) + else: + 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) + 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: # noqa + 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) except: assert count < THRESHOLD count += 1 - for i in range(10): - self.do() - sleep(0.02) + self.do() - def loop(self): - checked = False + 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: self.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 + lines = str(line, 'UTF-8').strip().split() + print(' '.join(lines[1:])) + else: + for line in lines[:5]: + line = str(line, 'UTF-8').strip().lower() + if 'banned' in line: + raise SyniToxError(line) + if 'error' in line and 'closing' in line: + 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 + + 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] 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:]) + + rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % + self._oargs.irc_chan, line, re.S) + 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']: + # 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_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')) + self.irc.send(bytes(sMSG+' NickServ IDENTIFY %s %s\r\n' + % (pwd,nick, ), 'UTF-8')) + elif email != '' and pwd: + LOG.info(bytes(sMSG+' NickServ REGISTER %s %s\r\n' + % (pwd, email,), 'UTF-8')) + 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(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') + + # 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, iMax=1000): + readable = False + waiti = 0 + while not readable: + waiti += 1 + readable, _, _ = select.select([self.irc], [], [], n/100.0 ) + if readable and len(readable) and readable[0]: return readable + self.do(n) + if waiti > iMax: break + return readable + + def iLoop(self): + group_connected = False + routed = None self.joined = False self.request = False - - try: + 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 + LOG.info(f"Looping for Tox and IRC connections") + if iCount < self._oargs.max_sleep: while True: - status = self.isconnected() - if not checked and status: - print('Connected to DHT.') - checked = True + iCount += 1 +# LOG.debug(f"Looping {iCount}") + b = self.bRouted() + if not b: + self.unroute() + 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 + elif b != routed or routed is None: + LOG.debug(f'Routed {iCount} - resetting count') + iDelay = 10 + routed = b + + dht_conneted = self.self_get_connection_status() + if not dht_conneted: + self.dht_init() + LOG.info(f'Not DHT connected {iCount} iterating {iDelay} seconds') + iDelay = iDelay + iDelay // 10 + self.do(iDelay) + #drop through + + if not group_connected and dht_conneted: + LOG.info('Connected to DHT.') + group_connected = 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) + r = self.group_reconnect(self.sGROUP_BOT_NUM) + LOG.info(f'Connected to group {r}') + except ctypes.ArgumentError as e: # noqa + self.bid = None - if checked and not status: - print('Disconnected from DHT.') - self.connect() - checked = False + 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_BOT_NUM + my_pk = self.group_self_get_public_key(num) + LOG.info(f'Connected to group as {my_pk[:8]}') - readable, _, _ = select.select([self.irc], [], [], 0.01) + if group_connected and not dht_conneted: + LOG.info('Disconnected from DHT.') + self.dht_init() + group_connected = False - if readable: - self.readbuffer += self.irc.recv(4096) - lines = self.readbuffer.split('\n') - self.readbuffer = lines.pop() + if not self.irc: + self.irc_init() + if not self.irc: + self.do(20) + continue - for line in lines: - rx = re.match(r':(.*?)!.*? PRIVMSG %s :(.*?)\r' % - CHANNEL, line, re.S) - if rx: - print('IRC> %s: %s' % rx.groups()) - msg = '[%s]: %s' % rx.groups() - content = rx.group(2) + + LOG.info(f'Waiting on IRC to {self._oargs.irc_host} on {self._oargs.irc_port}') - 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)) + readable = self.spin(20) + if not readable or not readable[0]: + LOG.info('Waited on IRC but nothing to read.') + iDelay = iDelay + iDelay // 10 + continue + try: + pass + except Exception as e: + 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 - if content.startswith('^'): - self.handle_command(content) + self.irc_readlines() + self.do(iDelay) + return 0 - l = line.rstrip().split() - if 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) + def quit(self): + self.del_callbacks() + self.save_to_file() - self.do() - except KeyboardInterrupt: - self.save_to_file('data') + 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: - self.irc_init() 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: - print('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') - def on_group_invite(self, friendid, type, data): + # 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) - print('Joined groupchat.') + nick = self._oargs.group_nick + self.tox_group_id = self.group_invite_accept(invite_data, friendid, nick) + LOG.info('Joined groupchat.') - def on_group_message(self, groupnumber, friendgroupnumber, message): - name = self.group_peername(groupnumber, friendgroupnumber) + 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: 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(sMSG+' %s :[%s]: %s\r\n' % + (self._oargs.irc_chan, name, message)) if message.startswith('^'): self.handle_command(message) - def on_group_action(self, groupnumber, friendgroupnumber, action): - name = self.group_peername(groupnumber, friendgroupnumber) - if len(name) and name != NAME: + def on_group_action(self, groupnumber, peer_id, action): + """old? message type action?""" + name = self.group_peername(groupnumber, peer_id) + 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' % - (CHANNEL, 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): - print('Friend request from %s: %s' % (pk, message)) - self.add_friend_norequest(pk) - print('Accepted.') + LOG.info('Friend request from %s: %s' % (pk, message)) + 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: - print('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' % (CHANNEL, 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')) 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 '): @@ -247,12 +1051,223 @@ 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]) + def is_data_encrypted(self, data): + return len(data) > 0 and self._toxes.is_data_encrypted(data) -t = SyncBot() -t.loop() + 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 + + try: + o = SyniTox(oArgs, oOpts) + __builtins__.app = o + o.start() + ret = o.iLoop() + o.quit() + 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 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 = 3 + else: + ret = 0 + return ret + +def oToxygenToxOptions(oArgs): + 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) + + #? tox_options.contents.log_callback = LOG + 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: + LOG.warn("No tox_options._options_pointer " +repr(tox_options._options_pointer)) + + 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, + help='Path to Tox profile - new groups will be saved there') + + CAcs = [] + for elt in lCAs: + if os.path.exists(elt): + CAcs.append(elt) + 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) + 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 ? + # oftcnet6xg6roj6d7id4y4cu6dchysacqj2ldgea73qzdagufflqxrid.onion:6697 + # 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', + help="IRC channel to join - include the #") + # + parser.add_argument('--irc_ssl', type=str, default='', + 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 (in PEM)", + default=CAfs[0]) + parser.add_argument('--irc_cadir', type=str, + help="Certificate Authority directory", + default=CAcs[0]) + 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='', + 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 - 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='', + 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: + 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): + + if lArgs is None: lArgs = [] + global oTOX_OARGS + oTOX_OARGS = oArgparse(lArgs) + + ts.clean_booleans(oTOX_OARGS) + vInitializeOargs() + + global oTOX_OPTIONS + oTOX_OPTIONS = oToxygenToxOptions(oTOX_OARGS) + + ts.vSetupLogging(oTOX_OARGS) +# ts.setup_logging(oArgs) + + return iMain(oTOX_OARGS, oTOX_OPTIONS) + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) + +# Ran 34 tests in 86.589s OK (skipped=12) diff --git a/tox-irc-sync_test.bash b/tox-irc-sync_test.bash new file mode 100644 index 0000000..b440ea8 --- /dev/null +++ b/tox-irc-sync_test.bash @@ -0,0 +1,266 @@ +#!/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 +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 || { + DBUG() { echo DEBUG $* ; } + INFO() { echo INFO $* ; } + WARN() { echo WARN $* ; } + 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 + +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 SSL TLSv1.3 ciphers available to the client. + TLS=2 +fi +[ $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 + # 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 + +declare -a RARGS +if [ "$DEBUG" = 1 ] ; then + RARGS=( + --log_level 10 + ) +else + RARGS=( + --log_level 20 + ) +fi +[ -n "$socks_proxy" ] && \ + RARGS+=( + --proxy_type 2 + --proxy_port 9050 + --proxy_host ${SOCKS_HOST} + --trace_enabled True +) +declare -a LARGS +LARGS=( + --irc_host $HOST + --irc_port $IRC_PORT + --irc_ssl "" + --irc_ident SyniTox + --irc_name SyniTox + --irc_nick $NICK + ) + +if [ $# -eq 0 -o "$1" = 1 ] && [ -n "$IRC_PORT" ] ; then + INFO No SSL + python3 tox-irc-sync.py "${LARGS[@]}" "${RARGS[@]}" + DBUG $? +fi + +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). +Other addresses for $HOST (not scanned): (null) +rDNS record for 130.239.18.116: solenoid.acc.umu.se + +PORT STATE SERVICE +$IRCS_PORT/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 +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_crt "$CRT" + --irc_key "$KEY" + # E178E7B9BD9E540278118193AD2C84DEF9B35E85 + --irc_fp "$FP" + --irc_cafile /usr/local/etc/ssl/cacert-testforge.pem + ) + +ip=`tor-resolve -4 $ONION` +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