diff --git a/README.md b/README.md index f894e0f..de88c36 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,3 @@ -# 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. +#Tox-Sync +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 a41e65d..9c386ec 100644 --- a/tox-irc-sync.py +++ b/tox-irc-sync.py @@ -1,1049 +1,245 @@ -# -*- mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*- - -import ctypes -import logging -import os -import pickle -import re -import select -import socket import sys -import traceback -from errno import errorcode -from random import shuffle +import socket +import string +import select +import re +import pickle + +from pytox import Tox, ToxAV + from time import sleep +from os.path import exists +from threading import Thread -from OpenSSL import SSL +SERVER = ['54.199.139.199', 33445, '7F9C31FE850E97CEFD4C4591DF93FC757C7C12549DDD55F8EEAECC34FE76C029'] +GROUP_BOT = '56A1ADE4B65B86BCD51CC73E2CD4E542179F47959FE3E0E21B4B0ACDADE51855D34D34D37CB5' +PWD = '' -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 +IRC_HOST = 'irc.freenode.net' +IRC_PORT = 6667 +NAME = NICK = IDENT = REALNAME = 'SyncBot' -try: - import support_testing as ts -except ImportError: - import wrapper_tests.support_testing as ts +CHANNEL = '#tox-ontopic' +MEMORY_DB = 'memory.pickle' -import wrapper.toxencryptsave as tox_encrypt_save +class AV(ToxAV): + def __init__(self, core, max_calls): + self.core = self.get_tox() + self.cs = None + self.call_type = self.TypeAudio -global LOG -LOG = logging.getLogger('app.'+'ts') -import warnings + def on_invite(self, idx): + self.cs = self.get_peer_csettings(idx, 0) + self.call_type = self.cs['call_type'] -warnings.filterwarnings('ignore') + 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)))) -class SyniToxError(BaseException): pass + self.answer(idx, self.call_type) + print('Answered, in call...') -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_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) -# 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) + def on_end(self, idx): + self.kill_transmission() -# 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}") + print('Call ended') - # 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_peer_timeout(self, idx): + self.stop_call() - if x509.get_subject().commonName == host: - return True + def on_audio_data(self, idx, size, data): + sys.stdout.write('.') + sys.stdout.flush() + self.send_audio(idx, size, 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 + def on_video_data(self, idx, width, height, data): + sys.stdout.write('*') + sys.stdout.flush() + self.send_video(idx, width, height, data) - return False +bot_toxname = 'SyncBot' - return ssl_verify +class SyncBot(Tox): + def __init__(self): + if exists('data'): + self.load_from_file('data') - -class SyniTox(Tox): - - def __init__(self, - oArgs, - oOpts, - GROUP_BOT_PK = '', - sMEMORY_DB = '' - ): - - 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 - - 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.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.readbuffer = '' self.tox_group_id = None - self.init_callbacks() - if os.path.exists(self.sMEMORY_DB): - with open(self.sMEMORY_DB, 'r') as f: + self.irc_init() + self.memory = {} + + if exists(MEMORY_DB): + with open(MEMORY_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): - 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 ") + 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)) - 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 connect(self): + print('connecting...') + self.bootstrap_from_address(SERVER[0], SERVER[1], SERVER[2]) - 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): + def ensure_exe(self, func, args): count = 0 THRESHOLD = 50 + while True: try: return func(*args) except: assert count < THRESHOLD count += 1 - self.do() + for i in range(10): + self.do() + sleep(0.02) - 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 + def loop(self): + checked = False self.joined = False self.request = False - 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: + + try: while 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 + status = self.isconnected() + if not checked and status: + print('Connected to DHT.') + checked = True try: - #? 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 + 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) - 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]}') + if checked and not status: + print('Disconnected from DHT.') + self.connect() + checked = False - if group_connected and not dht_conneted: - LOG.info('Disconnected from DHT.') - self.dht_init() - group_connected = False + readable, _, _ = select.select([self.irc], [], [], 0.01) - if not self.irc: - self.irc_init() - if not self.irc: - self.do(20) - continue + if readable: + self.readbuffer += self.irc.recv(4096) + lines = self.readbuffer.split('\n') + self.readbuffer = lines.pop() - - LOG.info(f'Waiting on IRC to {self._oargs.irc_host} on {self._oargs.irc_port}') + 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) - 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[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)) - self.irc_readlines() - self.do(iDelay) - return 0 + if content.startswith('^'): + self.handle_command(content) - def quit(self): - self.del_callbacks() - self.save_to_file() + 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 save_to_file(self): - pass + self.do() + except KeyboardInterrupt: + self.save_to_file('data') def irc_send(self, msg): success = False while not success: try: - self.irc.send(bytes(msg, 'UTF-8')) + self.irc.send(msg) 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: - LOG.info('Groupbot online, trying to get invited to group chat.') + print('Groupbot online, trying to join group chat.') self.request = True - 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') + self.ensure_exe(self.send_message, (self.bid, 'invite')) - # gi_wrapped - def on_group_invite(self, friendid, invite_data, user_data): + def on_group_invite(self, friendid, type, data): if not self.joined: self.joined = True - nick = self._oargs.group_nick - self.tox_group_id = self.group_invite_accept(invite_data, friendid, nick) - LOG.info('Joined groupchat.') + self.tox_group_id = self.join_groupchat(friendid, data) + print('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) + def on_group_message(self, groupnumber, friendgroupnumber, message): + name = self.group_peername(groupnumber, friendgroupnumber) if len(name) and name != NAME: print('TOX> %s: %s' % (name, message)) if message.startswith('>'): message = '\x0309%s\x03' % message - self.irc_send(sMSG+' %s :[%s]: %s\r\n' % - (self._oargs.irc_chan, name, message)) + + self.irc_send('PRIVMSG %s :[%s]: %s\r\n' % + (CHANNEL, 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 name and name != NAME: + def on_group_action(self, groupnumber, friendgroupnumber, action): + name = self.group_peername(groupnumber, friendgroupnumber) + if len(name) and name != NAME: 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('PRIVMSG %s :\x01ACTION [%s]: %s\x01\r\n' % + (CHANNEL, name, action)) def on_friend_request(self, pk, message): - LOG.info('Friend request from %s: %s' % (pk, message)) - self.friend_add_norequest(pk) - LOG.info('Accepted.') + print('Friend request from %s: %s' % (pk, message)) + self.add_friend_norequest(pk) + print('Accepted.') def on_friend_message(self, friendid, message): - if message.startswith('invite'): + if message == 'invite': if not self.tox_group_id is None: - LOG.info('Inviting %s' % self.friend_get_name(friendid)) - self.group_invite_friend(self.sGROUP_BOT_NUM, friendid) + print('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.' - type_ = TOX_MESSAGE_TYPE['NORMAL'] - self.ensure_exe(self.friend_send_message, friendid, type_, message) + self.ensure_exe(self.send_message, (friendid, message)) 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.ensure_exe(self.group_message_send, (self.tox_group_id, content)) + self.irc_send('PRIVMSG %s :%s\r\n' % (CHANNEL, content)) def handle_command(self, cmd): cmd = cmd[1:] if cmd in ['syncbot', 'echobot']: - self.send_both(self.self_get_address()) + self.send_both(self.get_address()) elif cmd == 'resync': sys.exit(0) elif cmd.startswith('remember '): @@ -1051,223 +247,12 @@ class SyniTox(Tox): subject = args[0] desc = ' '.join(args[1:]) self.memory[subject] = desc - if self.sMEMORY_DB: - with open(self.sMEMORY_DB, 'w') as f: - pickle.dump(self.memory, f) + with open(MEMORY_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) - 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) +t = SyncBot() +t.loop() diff --git a/tox-irc-sync_test.bash b/tox-irc-sync_test.bash deleted file mode 100644 index b440ea8..0000000 --- a/tox-irc-sync_test.bash +++ /dev/null @@ -1,266 +0,0 @@ -#!/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