9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz

This commit is contained in:
emdee 2022-10-19 18:09:31 +00:00
parent a5b3822651
commit 4c1b226bff
1045 changed files with 753037 additions and 18 deletions

View file

View file

@ -0,0 +1,857 @@
# Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
# 2006 Dimitur Kirov <dkirov@gmail.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import socket
import ssl
import errno
import sys
import os
import logging
from unittest.mock import Mock
import nbxmpp
from nbxmpp import old_dispatcher as dispatcher
from nbxmpp import simplexml
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.plugin import PlugIn
from nbxmpp.idlequeue import IdleObject
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common.zeroconf import zeroconf
from gajim.common.zeroconf import roster_zeroconf
log = logging.getLogger('gajim.c.z.client_zeroconf')
MAX_BUFF_LEN = 65536
TYPE_SERVER, TYPE_CLIENT = range(2)
# wait XX sec to establish a connection
CONNECT_TIMEOUT_SECONDS = 10
# after XX sec with no activity, close the stream
ACTIVITY_TIMEOUT_SECONDS = 30
class ZeroconfListener(IdleObject):
def __init__(self, port, conn_holder):
"""
Handle all incoming connections on ('0.0.0.0', port)
"""
self.port = port
self.queue_idx = -1
#~ self.queue = None
self.started = False
self._sock = None
self.fd = -1
self.caller = conn_holder.caller
self.conn_holder = conn_holder
def bind(self):
flags = socket.AI_PASSIVE
if hasattr(socket, 'AI_ADDRCONFIG'):
flags |= socket.AI_ADDRCONFIG
ai = socket.getaddrinfo(None, self.port, socket.AF_UNSPEC,
socket.SOCK_STREAM, 0, flags)[0]
self._serv = socket.socket(ai[0], ai[1])
self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
if os.name == 'nt':
if sys.getwindowsversion().major >= 6: # Win Vista +
# 47 is socket.IPPROTO_IPV6
# 27 is socket.IPV6_V6ONLY under windows, but not defined ...
self._serv.setsockopt(41, 27, 0)
# will fail when port is busy, or we don't have rights to bind
try:
self._serv.bind((ai[4][0], self.port))
except Exception:
# unable to bind, show error dialog
return None
self._serv.listen(socket.SOMAXCONN)
self._serv.setblocking(False)
self.fd = self._serv.fileno()
app.idlequeue.plug_idle(self, False, True)
self.started = True
def pollend(self):
"""
Called when we stop listening on (host, port)
"""
self.disconnect()
def pollin(self):
"""
Accept a new incoming connection and notify queue
"""
sock = self.accept_conn()
# loop through roster to find who has connected to us
from_jid = None
ipaddr = sock[1][0]
for jid in self.conn_holder.getRoster().keys():
entry = self.conn_holder.getRoster().getItem(jid)
for address in entry['addresses']:
if address['address'] == ipaddr:
from_jid = jid
break
P2PClient(sock[0], [{'host': ipaddr, 'address': ipaddr, 'port': sock[1][1]}], self.conn_holder, [], from_jid)
def disconnect(self, message=''):
"""
Free all resources, we are not listening anymore
"""
log.info('Disconnecting ZeroconfListener: %s', message)
app.idlequeue.remove_timeout(self.fd)
app.idlequeue.unplug_idle(self.fd)
self.fd = -1
self.started = False
try:
self._serv.close()
except socket.error:
pass
self.conn_holder.kill_all_connections()
def accept_conn(self):
"""
Accept a new incoming connection
"""
_sock = self._serv.accept()
_sock[0].setblocking(False)
return _sock
class P2PClient(IdleObject):
def __init__(self, _sock, addresses, conn_holder, stanzaqueue, to=None,
on_ok=None, on_not_ok=None):
self._owner = self
self.Namespace = 'jabber:client'
self.protocol_type = 'XMPP'
self.defaultNamespace = self.Namespace
self.Smacks = Mock()
self._component = 0
self._registered_name = None
self._caller = conn_holder.caller
self.conn_holder = conn_holder
self.stanzaqueue = stanzaqueue
self.to = to
#self.Server = addresses[0]['host']
self.on_ok = on_ok
self.on_not_ok = on_not_ok
self.Connection = None
self.sock_hash = None
if _sock:
self.sock_type = TYPE_SERVER
else:
self.sock_type = TYPE_CLIENT
self.fd = -1
conn = P2PConnection('', _sock, addresses, self._caller,
self.on_connect, self)
self.Server = conn.host # set Server to the last host name / address tried
if not self.conn_holder:
# An error occurred, disconnect() has been called
if on_not_ok:
on_not_ok('Connection to host could not be established.')
return
self.sock_hash = conn._sock.__hash__
self.fd = conn.fd
self.conn_holder.add_connection(self, self.Server, conn.port, self.to)
# count messages in queue
for val in self.stanzaqueue:
stanza, is_message = val
if is_message:
if self.fd == -1:
if on_not_ok:
on_not_ok(
'Connection to host could not be established.')
return
thread_id = stanza.getThread()
id_ = stanza.getID()
if not id_:
id_ = generate_id()
if self.fd in self.conn_holder.ids_of_awaiting_messages:
self.conn_holder.ids_of_awaiting_messages[self.fd].append((
id_, thread_id))
else:
self.conn_holder.ids_of_awaiting_messages[self.fd] = [(id_,
thread_id)]
self.on_responses = {}
def get_bound_jid(self):
return self._caller.get_own_jid()
def add_stanza(self, stanza, is_message=False):
if self.Connection:
if self.Connection.state == -1:
return False
self.send(stanza, is_message)
else:
self.stanzaqueue.append((stanza, is_message))
if is_message:
thread_id = stanza.getThread()
id_ = stanza.getID()
if not id_:
id_ = generate_id()
if self.fd in self.conn_holder.ids_of_awaiting_messages:
self.conn_holder.ids_of_awaiting_messages[self.fd].append((id_,
thread_id))
else:
self.conn_holder.ids_of_awaiting_messages[self.fd] = [(id_,
thread_id)]
return True
def on_message_sent(self, connection_id):
id_, _thread_id = \
self.conn_holder.ids_of_awaiting_messages[connection_id].pop(0)
if self.on_ok:
self.on_ok(id_)
# use on_ok only on first message. For others it's called in
# ClientZeroconf
self.on_ok = None
def on_connect(self, conn):
self.Connection = conn
self.Connection.PlugIn(self)
dispatcher.Dispatcher().PlugIn(self)
self._register_handlers()
def StreamInit(self):
"""
Send an initial stream header
"""
self.Dispatcher.Stream = simplexml.NodeBuilder()
self.Dispatcher.Stream._dispatch_depth = 2
self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch
self.Dispatcher.Stream.stream_header_received = self._check_stream_start
self.Dispatcher.Stream.features = None
if self.sock_type == TYPE_CLIENT:
self.send_stream_header()
def send_stream_header(self):
self.Dispatcher._metastream = nbxmpp.Node('stream:stream')
self.Dispatcher._metastream.setNamespace(self.Namespace)
self.Dispatcher._metastream.setAttr('version', '1.0')
self.Dispatcher._metastream.setAttr('xmlns:stream', Namespace.STREAMS)
self.Dispatcher._metastream.setAttr('from',
self.conn_holder.zeroconf.name)
if self.to:
self.Dispatcher._metastream.setAttr('to', self.to)
self.Dispatcher.send("<?xml version='1.0'?>%s>" % str(
self.Dispatcher._metastream)[:-2])
def _check_stream_start(self, ns, tag, attrs):
if ns != Namespace.STREAMS or tag != 'stream':
log.error('Incorrect stream start: (%s,%s).Terminating!',
tag, ns)
self.Connection.disconnect()
if self.on_not_ok:
self.on_not_ok('Connection to host could not be established: '
'Incorrect answer from server.')
return
if self.sock_type == TYPE_SERVER:
if 'from' in attrs:
self.to = attrs['from']
self.send_stream_header()
if 'version' in attrs and attrs['version'] == '1.0':
# other part supports stream features
features = nbxmpp.Node('stream:features')
self.Dispatcher.send(features)
while self.stanzaqueue:
stanza, is_message = self.stanzaqueue.pop(0)
self.send(stanza, is_message)
elif self.sock_type == TYPE_CLIENT:
while self.stanzaqueue:
stanza, is_message = self.stanzaqueue.pop(0)
self.send(stanza, is_message)
def on_disconnect(self):
if self.conn_holder:
if self.fd in self.conn_holder.ids_of_awaiting_messages:
del self.conn_holder.ids_of_awaiting_messages[self.fd]
self.conn_holder.remove_connection(self.sock_hash)
if 'Dispatcher' in self.__dict__:
self._caller._unregister_new_handlers(self)
self.Dispatcher.PlugOut()
if 'P2PConnection' in self.__dict__:
self.P2PConnection.PlugOut()
self.Connection = None
self._caller = None
self.conn_holder = None
def force_disconnect(self):
if self.Connection:
self.disconnect()
else:
self.on_disconnect()
def _on_receive_document_attrs(self, data):
if data:
self.Dispatcher.ProcessNonBlocking(data)
if not hasattr(self, 'Dispatcher') or \
self.Dispatcher.Stream._document_attrs is None:
return
self.onreceive(None)
if 'version' in self.Dispatcher.Stream._document_attrs and \
self.Dispatcher.Stream._document_attrs['version'] == '1.0':
#~ self.onreceive(self._on_receive_stream_features)
#XXX continue with TLS
return
self.onreceive(None)
return True
def remove_timeout(self):
pass
def _register_handlers(self):
self._caller.peerhost = self.Connection._sock.getsockname()
self.RegisterHandler(*StanzaHandler(name='message',
callback=self._caller._messageCB))
self.RegisterHandler(*StanzaHandler(name='message',
typ='error',
callback=self._caller._message_error_received))
self._caller._register_new_handlers(self)
class P2PConnection(IdleObject, PlugIn):
def __init__(self, sock_hash, _sock, addresses=None, caller=None,
on_connect=None, client=None):
IdleObject.__init__(self)
self._owner = client
PlugIn.__init__(self)
self.sendqueue = []
self.sendbuff = None
self.buff_is_message = False
self._sock = _sock
self.sock_hash = None
self.addresses = addresses
self.on_connect = on_connect
self.client = client
self.writable = False
self.readable = False
self._exported_methods = [self.send, self.disconnect, self.onreceive]
self.on_receive = None
if _sock:
self.host = addresses[0]['host']
self.port = addresses[0]['port']
self._sock = _sock
self.state = 1
self._sock.setblocking(False)
self.fd = self._sock.fileno()
self.on_connect(self)
else:
self.state = 0
self.addresses_ = self.addresses
self.get_next_addrinfo()
def get_next_addrinfo(self):
address = self.addresses_.pop(0)
self.host = address['host']
self.port = address['port']
try:
self.ais = socket.getaddrinfo(address['host'], address['port'], socket.AF_UNSPEC,
socket.SOCK_STREAM)
except socket.gaierror as e:
log.info('Lookup failure for %s: %s[%s]', self.host, e[1],
repr(e[0]), exc_info=True)
if self.addresses_:
return self.get_next_addrinfo()
else:
self.connect_to_next_ip()
def connect_to_next_ip(self):
if not self.ais:
log.error('Connection failure to %s', str(self.host), exc_info=True)
if self.addresses_:
return self.get_next_addrinfo()
self.disconnect()
return
ai = self.ais.pop(0)
log.info('Trying to connect to %s through %s:%s',
str(self.host), ai[4][0], ai[4][1], exc_info=True)
try:
self._sock = socket.socket(*ai[:3])
self._sock.setblocking(False)
self._server = ai[4]
except socket.error:
if sys.exc_value[0] != errno.EINPROGRESS:
# for all errors, we try other addresses
self.connect_to_next_ip()
return
self.fd = self._sock.fileno()
app.idlequeue.plug_idle(self, True, False)
self.set_timeout(CONNECT_TIMEOUT_SECONDS)
self.do_connect()
def set_timeout(self, timeout):
app.idlequeue.remove_timeout(self.fd)
if self.state >= 0:
app.idlequeue.set_read_timeout(self.fd, timeout)
def plugin(self, owner):
self.onreceive(owner._on_receive_document_attrs)
self._plug_idle()
return True
def plugout(self):
"""
Disconnect from the remote server and unregister self.disconnected
method from the owner's dispatcher
"""
self.disconnect()
self._owner = None
def onreceive(self, recv_handler):
if not recv_handler:
if hasattr(self._owner, 'Dispatcher'):
self.on_receive = self._owner.Dispatcher.ProcessNonBlocking
else:
self.on_receive = None
return
_tmp = self.on_receive
# make sure this cb is not overridden by recursive calls
if not recv_handler(None) and _tmp == self.on_receive:
self.on_receive = recv_handler
def send(self, packet, is_message=False, now=False):
"""
Append stanza to the queue of messages to be send if now is False, else
send it instantly
"""
if self.state <= 0:
return
r = str(packet).encode('utf-8')
if now:
self.sendqueue.insert(0, (r, is_message))
self._do_send()
else:
self.sendqueue.append((r, is_message))
self._plug_idle()
def read_timeout(self):
ids = self.client.conn_holder.ids_of_awaiting_messages
if self.fd in ids and ids[self.fd]:
for (_id, thread_id) in ids[self.fd]:
if hasattr(self._owner, 'Dispatcher'):
self._owner.Dispatcher.Event('', 'DATA ERROR', (
self.client.to, thread_id))
else:
self._owner.on_not_ok('connection timeout')
ids[self.fd] = []
self.pollend()
def do_connect(self):
errnum = 0
try:
self._sock.connect(self._server[:2])
self._sock.setblocking(False)
except Exception as ee:
errnum = ee.errno
errstr = ee.strerror
errors = (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK)
if 'WSAEINVAL' in errno.__dict__:
errors += (errno.WSAEINVAL,)
if errnum in errors:
return
# win32 needs this
if errnum not in (0, 10056, errno.EISCONN) or self.state != 0:
log.error('Could not connect to %s: %s [%s]', str(self.host),
errnum, errstr)
self.connect_to_next_ip()
return
# socket is already connected
self._sock.setblocking(False)
self.state = 1 # connected
# we are connected
self.on_connect(self)
def pollout(self):
if self.state == 0:
self.do_connect()
return
app.idlequeue.remove_timeout(self.fd)
self._do_send()
def pollend(self):
if self.state == 0: # error in connect()?
#self.disconnect()
self.connect_to_next_ip()
else:
self.state = -1
self.disconnect()
def pollin(self):
"""
Reads all pending incoming data. Call owner's disconnected() method if
appropriate
"""
received = ''
errnum = 0
try:
# get as many bites, as possible, but not more than RECV_BUFSIZE
received = self._sock.recv(MAX_BUFF_LEN)
except Exception as e:
errnum = e.errno
# "received" will be empty anyhow
if errnum == ssl.SSL_ERROR_WANT_READ:
pass
elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]:
self.pollend()
# dont process result, in case it will raise an error
return
elif not received:
if errnum != ssl.SSL_ERROR_EOF:
# 8 EOF occurred in violation of protocol
self.pollend()
if self.state >= 0:
self.disconnect()
return
if self.state < 0:
return
received = received.decode('utf-8')
if self.on_receive:
if self._owner.sock_type == TYPE_CLIENT:
self.set_timeout(ACTIVITY_TIMEOUT_SECONDS)
if received.strip():
log.debug('received: %s', received)
if hasattr(self._owner, 'Dispatcher'):
self._owner.Dispatcher.Event('', 'DATA RECEIVED', received)
self.on_receive(received)
else:
# This should never happed, so we need the debug
log.error('Unhandled data received: %s', received)
self.disconnect()
return True
def disconnect(self, message=''):
"""
Close the socket
"""
app.idlequeue.remove_timeout(self.fd)
app.idlequeue.unplug_idle(self.fd)
try:
self._sock.shutdown(socket.SHUT_RDWR)
self._sock.close()
except socket.error:
# socket is already closed
pass
self.fd = -1
self.state = -1
if self._owner:
self._owner.on_disconnect()
def _do_send(self):
if not self.sendbuff:
if not self.sendqueue:
return None # nothing to send
self.sendbuff, self.buff_is_message = self.sendqueue.pop(0)
self.sent_data = self.sendbuff
try:
send_count = self._sock.send(self.sendbuff)
if send_count:
self.sendbuff = self.sendbuff[send_count:]
if not self.sendbuff and not self.sendqueue:
if self.state < 0:
app.idlequeue.unplug_idle(self.fd)
self._on_send()
self.disconnect()
return
# we are not waiting for write
self._plug_idle()
self._on_send()
except socket.error as e:
if e.errno == ssl.SSL_ERROR_WANT_WRITE:
return True
if self.state < 0:
self.disconnect()
return
self._on_send_failure()
return
if self._owner.sock_type == TYPE_CLIENT:
self.set_timeout(ACTIVITY_TIMEOUT_SECONDS)
return True
def _plug_idle(self):
readable = self.state != 0
writable = self.sendqueue or self.sendbuff
if self.writable != writable or self.readable != readable:
app.idlequeue.plug_idle(self, writable, readable)
def _on_send(self):
if self.sent_data and self.sent_data.strip():
log.debug('sent: %s', self.sent_data)
if hasattr(self._owner, 'Dispatcher'):
self._owner.Dispatcher.Event(
'', 'DATA SENT', self.sent_data.decode('utf-8'))
self.sent_data = None
if self.buff_is_message:
self._owner.on_message_sent(self.fd)
self.buff_is_message = False
def _on_send_failure(self):
log.error('Socket error while sending data')
self._owner.on_disconnect()
self.sent_data = None
class ClientZeroconf:
def __init__(self, caller):
self.caller = caller
self.zeroconf = None
self.roster = None
self.last_msg = ''
self.connections = {}
self.recipient_to_hash = {}
self.ip_to_hash = {}
self.hash_to_port = {}
self.listener = None
self.ids_of_awaiting_messages = {}
self.disconnect_handlers = []
self.disconnecting = False
def connect(self, show, msg):
self.port = self.start_listener(self.caller.port)
if not self.port:
return False
self.zeroconf_init(show, msg)
if not self.zeroconf.connect():
self.disconnect()
return None
self.roster = roster_zeroconf.Roster(self.zeroconf)
return True
def remove_announce(self):
if self.zeroconf:
return self.zeroconf.remove_announce()
def announce(self):
if self.zeroconf:
return self.zeroconf.announce()
def set_show_msg(self, show, msg):
if self.zeroconf:
self.zeroconf.txt['msg'] = msg
self.last_msg = msg
return self.zeroconf.update_txt(show)
def resolve_all(self):
if self.zeroconf:
return self.zeroconf.resolve_all()
def reannounce(self, txt):
self.remove_announce()
self.zeroconf.txt = txt
self.zeroconf.port = self.port
self.zeroconf.username = self.caller.username
return self.announce()
def zeroconf_init(self, show, msg):
self.zeroconf = zeroconf.Zeroconf(self.caller._on_new_service,
self.caller._on_remove_service, self.caller._on_name_conflictCB,
self.caller._on_disconnect, self.caller._on_error,
self.caller.username, self.caller.host, self.port)
self.zeroconf.txt['msg'] = msg
self.zeroconf.txt['status'] = show
self.zeroconf.txt['1st'] = self.caller.first
self.zeroconf.txt['last'] = self.caller.last
self.zeroconf.txt['jid'] = self.caller.jabber_id
self.zeroconf.txt['email'] = self.caller.email
self.zeroconf.username = self.caller.username
self.zeroconf.host = self.caller.host
self.zeroconf.port = self.port
self.last_msg = msg
def disconnect(self):
# to avoid recursive calls
if self.disconnecting:
return
if self.listener:
self.listener.disconnect()
self.listener = None
if self.zeroconf:
self.zeroconf.disconnect()
self.zeroconf = None
if self.roster:
self.roster.zeroconf = None
self.roster._data = None
self.roster = None
self.disconnecting = True
for i in reversed(self.disconnect_handlers):
log.debug('Calling disconnect handler %s', i)
i()
self.disconnecting = False
def start_disconnect(self):
self.disconnect()
def kill_all_connections(self):
for connection in list(self.connections.values()):
connection.force_disconnect()
def add_connection(self, connection, ip, port, recipient):
sock_hash = connection.sock_hash
if sock_hash not in self.connections:
self.connections[sock_hash] = connection
self.ip_to_hash[ip] = sock_hash
self.hash_to_port[sock_hash] = port
if recipient:
self.recipient_to_hash[recipient] = sock_hash
def remove_connection(self, sock_hash):
if sock_hash in self.connections:
del self.connections[sock_hash]
for i in self.recipient_to_hash:
if self.recipient_to_hash[i] == sock_hash:
del self.recipient_to_hash[i]
break
for i in self.ip_to_hash:
if self.ip_to_hash[i] == sock_hash:
del self.ip_to_hash[i]
break
if sock_hash in self.hash_to_port:
del self.hash_to_port[sock_hash]
def start_listener(self, port):
for p in range(port, port + 5):
self.listener = ZeroconfListener(p, self)
self.listener.bind()
if self.listener.started:
return p
self.listener = None
return False
def getRoster(self):
if self.roster:
return self.roster.getRoster()
return {}
def send(self, stanza, is_message=False, now=False, on_ok=None,
on_not_ok=None):
to = stanza.getTo()
if to is None:
# Cant send undirected stanza over Zeroconf.
return -1
to = to.bare
stanza.setFrom(self.roster.zeroconf.name)
try:
item = self.roster[to]
except KeyError:
# Contact offline
return -1
# look for hashed connections
if to in self.recipient_to_hash:
conn = self.connections[self.recipient_to_hash[to]]
id_ = stanza.getID() or ''
if conn.add_stanza(stanza, is_message):
if on_ok:
on_ok(id_)
return
the_address = None
for address in item['addresses']:
if address['address'] in self.ip_to_hash:
the_address = address
if the_address and the_address['address'] in self.ip_to_hash:
hash_ = self.ip_to_hash[the_address['address']]
if self.hash_to_port[hash_] == the_address['port']:
conn = self.connections[hash_]
id_ = stanza.getID() or ''
if conn.add_stanza(stanza, is_message):
if on_ok:
on_ok(id_)
return
# otherwise open new connection
if not stanza.getID():
stanza.setID('zero')
addresses_ = []
for address in item['addresses']:
addresses_ += [{'host': address['address'], 'address': address['address'], 'port': address['port']}]
P2PClient(None, addresses_, self,
[(stanza, is_message)], to, on_ok=on_ok, on_not_ok=on_not_ok)
def RegisterDisconnectHandler(self, handler):
"""
Register handler that will be called on disconnect
"""
self.disconnect_handlers.append(handler)
def UnregisterDisconnectHandler(self, handler):
"""
Unregister handler that is called on disconnect
"""
self.disconnect_handlers.remove(handler)
def SendAndWaitForResponse(self, stanza, timeout=None, func=None,
args=None):
"""
Send stanza and wait for recipient's response to it. Will call
transports on_timeout callback if response is not retrieved in time
Be aware: Only timeout of latest call of SendAndWait is active.
"""
# if timeout is None:
# timeout = DEFAULT_TIMEOUT_SECONDS
def on_ok(_waitid):
# if timeout:
# self._owner.set_timeout(timeout)
to = stanza.getTo()
to = app.get_jid_without_resource(to)
try:
item = self.roster[to]
except KeyError:
# Contact offline
item = None
conn = None
if to in self.recipient_to_hash:
conn = self.connections[self.recipient_to_hash[to]]
elif item:
the_address = None
for address in item['addresses']:
if address['address'] in self.ip_to_hash:
the_address = address
if the_address and the_address['address'] in self.ip_to_hash:
hash_ = self.ip_to_hash[the_address['address']]
if self.hash_to_port[hash_] == the_address['port']:
conn = self.connections[hash_]
if func:
conn.Dispatcher.on_responses[_waitid] = (func, args)
conn.onreceive(conn.Dispatcher._WaitForData)
conn.Dispatcher._expected[_waitid] = None
self.send(stanza, on_ok=on_ok)
def SendAndCallForResponse(self, stanza, func=None, args=None):
"""
Put stanza on the wire and call back when recipient replies. Additional
callback arguments can be specified in args.
"""
self.SendAndWaitForResponse(stanza, 0, func, args)

View file

@ -0,0 +1,127 @@
# Contributors for this file:
# - Yann Leboulanger <asterix@lagaule.org>
# - Nikos Kouremenos <nkour@jabber.org>
# - Dimitur Kirov <dkirov@gmail.com>
# - Travis Shirk <travis@pobox.com>
# - Stefan Bethge <stefan@lanpartei.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import time
import logging
from gajim.common import app
from gajim.common import connection_handlers
from gajim.common.helpers import AdditionalDataDict
from gajim.common.nec import NetworkEvent
from gajim.common.modules.util import get_eme_message
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml
log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf')
class ConnectionHandlersZeroconf(connection_handlers.ConnectionHandlersBase):
def __init__(self):
connection_handlers.ConnectionHandlersBase.__init__(self)
def _messageCB(self, con, stanza, properties):
"""
Called when we receive a message
"""
if properties.type.is_error:
return
log.info('Zeroconf MessageCB')
# Dont trust from attr set by sender
stanza.setFrom(con._owner.to)
app.nec.push_incoming_event(NetworkEvent(
'raw-message-received',
conn=self,
stanza=stanza,
account=self.name))
type_ = stanza.getType()
if type_ is None:
type_ = 'normal'
id_ = stanza.getID()
fjid = str(stanza.getFrom())
jid, resource = app.get_room_and_nick_from_fjid(fjid)
msgtxt = stanza.getBody()
session = self.get_or_create_session(fjid, properties.thread)
if properties.thread and not session.received_thread_id:
session.received_thread_id = True
timestamp = time.time()
session.last_receive = timestamp
additional_data = AdditionalDataDict()
parse_oob(properties, additional_data)
parse_xhtml(properties, additional_data)
if properties.is_encrypted:
additional_data['encrypted'] = properties.encrypted.additional_data
else:
if properties.eme is not None:
msgtxt = get_eme_message(properties.eme)
event_attr = {
'conn': self,
'stanza': stanza,
'account': self.name,
'additional_data': additional_data,
'timestamp': time.time(),
'fjid': fjid,
'jid': jid,
'resource': resource,
'unique_id': id_,
'correct_id': parse_correction(properties),
'msgtxt': msgtxt,
'session': session,
'gc_control': None,
'popup': False,
'msg_log_id': None,
'displaymarking': None,
'stanza_id': id_,
'properties': properties,
}
app.nec.push_incoming_event(
NetworkEvent('decrypted-message-received', **event_attr))
def _message_error_received(self, _con, _stanza, properties):
log.info(properties.error)
app.storage.archive.set_message_error(app.get_jid_from_account(self.name),
properties.jid,
properties.id,
properties.error)
app.nec.push_incoming_event(
NetworkEvent('message-error',
account=self.name,
jid=properties.jid,
message_id=properties.id,
error=properties.error))

View file

@ -0,0 +1,450 @@
# Contributors for this file:
# - Yann Leboulanger <asterix@lagaule.org>
# - Nikos Kouremenos <nkour@jabber.org>
# - Dimitur Kirov <dkirov@gmail.com>
# - Travis Shirk <travis@pobox.com>
# - Stefan Bethge <stefan@lanpartei.de>
#
# Copyright (C) 2003-2014 Yann Leboulanger <asterix@lagaule.org>
# Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org>
# Copyright (C) 2006 Nikos Kouremenos <nkour@jabber.org>
# Dimitur Kirov <dkirov@gmail.com>
# Travis Shirk <travis@pobox.com>
# Norman Rasmussen <norman@rasmussen.co.za>
# Stefan Bethge <stefan@lanpartei.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import socket
import getpass
import logging
import time
import nbxmpp
from gi.repository import GLib
from gajim.common import app
from gajim.common import modules
from gajim.common.nec import NetworkEvent
from gajim.common.i18n import _
from gajim.common.const import ClientState
from gajim.common.connection import CommonConnection
from gajim.common.zeroconf import client_zeroconf
from gajim.common.zeroconf import zeroconf
from gajim.common.zeroconf.connection_handlers_zeroconf import ConnectionHandlersZeroconf
from gajim.common.connection_handlers_events import OurShowEvent
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.connection_handlers_events import ConnectionLostEvent
from gajim.common.connection_handlers_events import MessageSentEvent
log = logging.getLogger('gajim.c.connection_zeroconf')
class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
def __init__(self, name):
ConnectionHandlersZeroconf.__init__(self)
# system username
self.username = None
self.server_resource = '' # zeroconf has no resource, fake an empty one
self.call_resolve_timeout = False
# we don't need a password, but must be non-empty
self.password = 'zeroconf'
self.autoconnect = False
self.httpupload = False
CommonConnection.__init__(self, name)
self.is_zeroconf = True
# Register all modules
modules.register_modules(self)
def get_config_values_or_default(self):
"""
Get name, host, port from config, or create zeroconf account with default
values
"""
self.host = socket.gethostname()
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'hostname',
self.host)
self.port = app.settings.get_account_setting(app.ZEROCONF_ACC_NAME,
'custom_port')
self.autoconnect = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'autoconnect')
self.sync_with_global_status = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'sync_with_global_status')
self.first = app.settings.get_account_setting(app.ZEROCONF_ACC_NAME,
'zeroconf_first_name')
self.last = app.settings.get_account_setting(app.ZEROCONF_ACC_NAME,
'zeroconf_last_name')
self.jabber_id = app.settings.get_account_setting(app.ZEROCONF_ACC_NAME,
'zeroconf_jabber_id')
self.email = app.settings.get_account_setting(app.ZEROCONF_ACC_NAME,
'zeroconf_email')
if not self.username:
self.username = getpass.getuser()
app.settings.set_account_setting(app.ZEROCONF_ACC_NAME,
'name',
self.username)
else:
self.username = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'name')
def get_own_jid(self, *args, **kwargs):
return nbxmpp.JID.from_string(self.username + '@' + self.host)
def reconnect(self):
# Do not try to reco while we are already trying
self.time_to_reconnect = None
log.debug('reconnect')
self.disconnect()
self.change_status(self._status, self._status_message)
def disable_account(self):
self.disconnect()
def _on_resolve_timeout(self):
if self._state.is_connected:
if not self.connection.resolve_all():
self.disconnect()
return False
diffs = self.roster.getDiffs()
for key in diffs:
self.roster.setItem(key)
app.nec.push_incoming_event(NetworkEvent(
'roster-info', conn=self, jid=key,
nickname=self.roster.getName(key), sub='both',
ask='no', groups=self.roster.getGroups(key),
avatar_sha=None))
self._on_presence(key)
#XXX open chat windows don't get refreshed (full name), add that
return self.call_resolve_timeout
# callbacks called from zeroconf
def _on_new_service(self, jid):
self.roster.setItem(jid)
app.nec.push_incoming_event(NetworkEvent(
'roster-info', conn=self, jid=jid,
nickname=self.roster.getName(jid), sub='both',
ask='no', groups=self.roster.getGroups(jid),
avatar_sha=None))
self._on_presence(jid)
def _on_remove_service(self, jid):
self.roster.delItem(jid)
# 'NOTIFY' (account, (jid, status, status message, resource, priority,
# timestamp))
self._on_presence(jid, show='offline', status='')
def _on_presence(self, jid, show=None, status=None):
if status is None:
status = self.roster.getMessage(jid)
if show is None:
show = self.roster.getStatus(jid)
ptype = 'unavailable' if show == 'offline' else None
event_attrs = {
'conn': self,
'prio': 0,
'need_add_in_roster': False,
'popup': False,
'ptype': ptype,
'jid': jid,
'resource': 'local',
'id_': None,
'fjid': jid,
'timestamp': 0,
'avatar_sha': None,
'user_nick': '',
'idle_time': None,
'show': show,
'new_show': show,
'old_show': 0,
'status': status,
'contact_list': [],
'contact': None,
}
event_ = NetworkEvent('presence-received', **event_attrs)
self._update_contact(event_)
app.nec.push_incoming_event(event_)
def _update_contact(self, event):
jid = event.jid
status_strings = ['offline', 'error', 'online', 'chat', 'away',
'xa', 'dnd']
event.new_show = status_strings.index(event.show)
contact = app.contacts.get_contact_strict(self.name, jid, '')
if contact is None:
contact = app.contacts.get_contact_strict(self.name, jid, 'local')
if contact.show in status_strings:
event.old_show = status_strings.index(contact.show)
# Update contact with presence data
contact.resource = 'local'
contact.show = event.show
contact.status = event.status
contact.priority = event.prio
contact.idle_time = event.idle_time
event.contact = contact
# It's not an agent
if event.old_show == 0 and event.new_show > 1:
if not jid in app.newly_added[self.name]:
app.newly_added[self.name].append(jid)
if jid in app.to_be_removed[self.name]:
app.to_be_removed[self.name].remove(jid)
elif event.old_show > 1 and event.new_show == 0 and self._state.is_connected:
if not jid in app.to_be_removed[self.name]:
app.to_be_removed[self.name].append(jid)
if jid in app.newly_added[self.name]:
app.newly_added[self.name].remove(jid)
if event.ptype == 'unavailable':
# TODO: This causes problems when another
# resource signs off!
self.get_module('Bytestream').stop_all_active_file_transfers(contact)
def _on_name_conflictCB(self, alt_name):
self.disconnect()
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
app.nec.push_incoming_event(
NetworkEvent('zeroconf-name-conflict',
conn=self,
alt_name=alt_name))
def _on_error(self, message):
app.nec.push_incoming_event(InformationEvent(
None, dialog_name='avahi-error', args=message))
def connect(self, show='online', msg=''):
self.get_config_values_or_default()
if not self.connection:
self.connection = client_zeroconf.ClientZeroconf(self)
if not zeroconf.test_zeroconf():
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
self._status = 'offline'
app.nec.push_incoming_event(ConnectionLostEvent(None,
conn=self, title=_('Could not connect to "%s"') % self.name,
msg=_('Please check if Avahi or Bonjour is installed.')))
self.disconnect()
return
result = self.connection.connect(show, msg)
if not result:
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
self._status = 'offline'
if result is False:
app.nec.push_incoming_event(ConnectionLostEvent(None,
conn=self, title=_('Could not start local service'),
msg=_('Unable to bind to port %d.') % self.port))
else: # result is None
app.nec.push_incoming_event(ConnectionLostEvent(None,
conn=self, title=_('Could not start local service'),
msg=_('Please check if avahi/bonjour-daemon is running.')))
self.disconnect()
return
else:
self.connection.announce()
self.roster = self.connection.getRoster()
app.nec.push_incoming_event(NetworkEvent('roster-received', conn=self,
roster=self.roster.copy(), received_from_server=True))
# display contacts already detected and resolved
for jid in self.roster.keys():
app.nec.push_incoming_event(NetworkEvent(
'roster-info', conn=self, jid=jid,
nickname=self.roster.getName(jid), sub='both',
ask='no', groups=self.roster.getGroups(jid),
avatar_sha=None))
self._on_presence(jid)
self._status = show
# refresh all contacts data every five seconds
self.call_resolve_timeout = True
GLib.timeout_add_seconds(5, self._on_resolve_timeout)
return True
def disconnect(self, reconnect=True, immediately=True):
log.info('Start disconnecting zeroconf')
if reconnect:
self.time_to_reconnect = 5
else:
self.time_to_reconnect = None
self._set_state(ClientState.DISCONNECTED)
if self.connection:
self.connection.disconnect()
self.connection = None
# stop calling the timeout
self.call_resolve_timeout = False
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
def _on_disconnect(self):
self._set_state(ClientState.DISCONNECTED)
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
def reannounce(self):
if self._state.is_connected:
txt = {}
txt['1st'] = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'zeroconf_first_name')
txt['last'] = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'zeroconf_last_name')
txt['jid'] = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id')
txt['email'] = app.settings.get_account_setting(
app.ZEROCONF_ACC_NAME, 'zeroconf_email')
self.connection.reannounce(txt)
def update_details(self):
if self.connection:
port = app.settings.get_account_setting(app.ZEROCONF_ACC_NAME,
'custom_port')
if port != self.port:
self.port = port
last_msg = self.connection.last_msg
self.disconnect()
if not self.connect(self._status, last_msg):
return
self.connection.announce()
else:
self.reannounce()
def connect_and_init(self, show, msg):
# to check for errors from zeroconf
check = True
if not self.connect(show, msg):
return
check = self.connection.announce()
# stay offline when zeroconf does something wrong
if check:
self._set_state(ClientState.CONNECTED)
app.nec.push_incoming_event(NetworkEvent('signed-in', conn=self))
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show=show))
else:
# show notification that avahi or system bus is down
self._set_state(ClientState.DISCONNECTED)
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
self._status = 'offline'
app.nec.push_incoming_event(ConnectionLostEvent(None, conn=self,
title=_('Could not change status of account "%s"') % self.name,
msg=_('Please check if avahi-daemon is running.')))
def _update_status(self, show, msg, idle_time=None):
if self.connection.set_show_msg(show, msg):
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show=show))
else:
# show notification that avahi or system bus is down
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show='offline'))
self._status = 'offline'
app.nec.push_incoming_event(ConnectionLostEvent(None, conn=self,
title=_('Could not change status of account "%s"') % self.name,
msg=_('Please check if avahi-daemon is running.')))
def send_message(self, message):
stanza = self.get_module('Message').build_message_stanza(message)
message.stanza = stanza
if message.contact is None:
# Only Single Message should have no contact
self._send_message(message)
return
method = message.contact.settings.get('encryption')
if not method:
self._send_message(message)
return
app.plugin_manager.extension_point('encrypt%s' % method,
self,
message,
self._send_message)
def _send_message(self, message):
def on_send_ok(stanza_id):
app.nec.push_incoming_event(
MessageSentEvent(None, jid=message.jid, **vars(message)))
self.get_module('Message').log_message(message)
def on_send_not_ok(reason):
reason += ' ' + _('Your message could not be sent.')
app.nec.push_incoming_event(NetworkEvent(
'zeroconf-error',
account=self.name,
jid=message.jid,
message=reason))
ret = self.connection.send(
message.stanza, message.message is not None,
on_ok=on_send_ok, on_not_ok=on_send_not_ok)
message.timestamp = time.time()
if ret == -1:
# Contact Offline
error_message = _(
'Contact is offline. Your message could not be sent.')
app.nec.push_incoming_event(NetworkEvent(
'zeroconf-error',
account=self.name,
jid=message.jid,
message=error_message))
return
def send_stanza(self, stanza):
# send a stanza untouched
if not self.connection:
return
if not isinstance(stanza, nbxmpp.Node):
stanza = nbxmpp.Protocol(node=stanza)
self.connection.send(stanza)
def _event_dispatcher(self, realm, event, data):
CommonConnection._event_dispatcher(self, realm, event, data)
if realm == '':
if event == nbxmpp.transports.DATA_ERROR:
frm = data[0]
error_message = _(
'Connection to host could not be established: '
'Timeout while sending data.')
app.nec.push_incoming_event(NetworkEvent(
'zeroconf-error',
account=self.name,
jid=frm,
message=error_message))
def cleanup(self):
pass

View file

@ -0,0 +1,160 @@
# Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from gajim.common.zeroconf.zeroconf import Constant, ConstantRI
class Roster:
def __init__(self, zeroconf):
self._data = None
self.zeroconf = zeroconf # our zeroconf instance
self.version = ''
def update_roster(self):
for val in self.zeroconf.get_contacts().values():
self.setItem(val[Constant.NAME])
def getRoster(self):
if self._data is None:
self._data = {}
self.update_roster()
return self
def getDiffs(self):
"""
Update the roster with new data and return dict with jid -> new status
pairs to do notifications and stuff
"""
diffs = {}
old_data = self._data.copy()
self.update_roster()
for key in old_data.keys():
if key in self._data:
if old_data[key] != self._data[key]:
diffs[key] = self._data[key]['status']
return diffs
def setItem(self, jid, name='', groups=''):
contact = self.zeroconf.get_contact(jid)
if not contact:
return
addresses = []
i = 0
for ri in contact[Constant.RESOLVED_INFO]:
addresses += [{}]
addresses[i]['host'] = ri[ConstantRI.HOST]
addresses[i]['address'] = ri[ConstantRI.ADDRESS]
addresses[i]['port'] = ri[ConstantRI.PORT]
i += 1
txt = contact[Constant.TXT]
self._data[jid] = {}
self._data[jid]['ask'] = 'none'
self._data[jid]['subscription'] = 'both'
self._data[jid]['groups'] = []
self._data[jid]['resources'] = {}
self._data[jid]['addresses'] = addresses
txt_dict = self.zeroconf.txt_array_to_dict(txt)
status = txt_dict.get('status', '')
if not status:
status = 'avail'
nm = txt_dict.get('1st', '')
if 'last' in txt_dict:
if nm != '':
nm += ' '
nm += txt_dict['last']
if nm:
self._data[jid]['name'] = nm
else:
self._data[jid]['name'] = jid
if status == 'avail':
status = 'online'
self._data[jid]['txt_dict'] = txt_dict
if 'msg' not in self._data[jid]['txt_dict']:
self._data[jid]['txt_dict']['msg'] = ''
self._data[jid]['status'] = status
self._data[jid]['show'] = status
def setItemMulti(self, items):
for i in items:
self.setItem(jid=i['jid'], name=i['name'], groups=i['groups'])
def delItem(self, jid):
if jid in self._data:
del self._data[jid]
def getItem(self, jid):
if jid in self._data:
return self._data[jid]
def __getitem__(self, jid):
return self._data[jid]
def __setitem__(self, jid, value):
self._data[jid] = value
def getItems(self):
# Return list of all [bare] JIDs that the roster currently tracks.
return self._data.keys()
def keys(self):
return self._data.keys()
def getRaw(self):
return self._data
def getResources(self, jid):
return {}
def getGroups(self, jid):
return self._data[jid]['groups']
def getName(self, jid):
if jid in self._data:
return self._data[jid]['name']
def getStatus(self, jid):
if jid in self._data:
return self._data[jid]['status']
def getMessage(self, jid):
if jid in self._data:
return self._data[jid]['txt_dict']['msg']
def getShow(self, jid):
return self.getStatus(jid)
def getPriority(self, jid):
return 5
def getSubscription(self, jid):
return 'both'
def Subscribe(self, jid):
pass
def Unsubscribe(self, jid):
pass
def Authorize(self, jid):
pass
def Unauthorize(self, jid):
pass
def copy(self):
return self._data.copy()

View file

@ -0,0 +1,63 @@
# Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Any # pylint: disable=unused-import
from enum import IntEnum, unique
@unique
class Constant(IntEnum):
NAME = 0
DOMAIN = 1
RESOLVED_INFO = 2
BARE_NAME = 3
TXT = 4
@unique
class ConstantRI(IntEnum):
INTERFACE = 0
PROTOCOL = 1
HOST = 2
APROTOCOL = 3
ADDRESS = 4
PORT = 5
def test_avahi():
try:
import gi
gi.require_version('Avahi', '0.6')
from gi.repository import Avahi # pylint: disable=unused-import
except (ImportError, ValueError):
return False
return True
def test_bonjour():
try:
import pybonjour # pylint: disable=unused-import
except Exception:
return False
return True
def test_zeroconf():
return test_avahi() or test_bonjour()
if test_avahi():
from gajim.common.zeroconf import zeroconf_avahi
Zeroconf = zeroconf_avahi.Zeroconf # type: Any
elif test_bonjour():
from gajim.common.zeroconf import zeroconf_bonjour
Zeroconf = zeroconf_bonjour.Zeroconf

View file

@ -0,0 +1,552 @@
# Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
from gi.repository import Avahi
from gi.repository import Gio
from gi.repository import GLib
from gajim.common.i18n import _
from gajim.common.zeroconf.zeroconf import Constant, ConstantRI
from gajim.common.zeroconf.zeroconf_avahi_const import DBUS_NAME
from gajim.common.zeroconf.zeroconf_avahi_const import DBUS_INTERFACE_SERVER
from gajim.common.zeroconf.zeroconf_avahi_const import DBUS_INTERFACE_ENTRY_GROUP
from gajim.common.zeroconf.zeroconf_avahi_const import DBUS_INTERFACE_DOMAIN_BROWSER
from gajim.common.zeroconf.zeroconf_avahi_const import ServerState
from gajim.common.zeroconf.zeroconf_avahi_const import EntryGroup
from gajim.common.zeroconf.zeroconf_avahi_const import DomainBrowser
from gajim.common.zeroconf.zeroconf_avahi_const import Protocol
from gajim.common.zeroconf.zeroconf_avahi_const import Interface
log = logging.getLogger('gajim.c.z.zeroconf_avahi')
class Zeroconf:
def __init__(self, new_service_cb, remove_service_cb, name_conflict_cb,
disconnected_cb, error_cb, name, host, port):
self.domain = None # specific domain to browse
self.stype = '_presence._tcp'
self.port = port # listening port that gets announced
self.username = name
self.host = host
self.txt = {}
self.name = None
self.connected = False
self.announced = False
#XXX these CBs should be set to None when we destroy the object
# (go offline), because they create a circular reference
self._new_service_cb = new_service_cb
self._remove_service_cb = remove_service_cb
self._name_conflict_cb = name_conflict_cb
self._disconnected_cb = disconnected_cb
self._error_cb = error_cb
self._server = None
self._avahi_client = None
self._service_browser = None
self._domain_browser = None
self._entrygroup = None
self._sb_connections = []
self._connections = {}
self._contacts = {}
self._invalid_self_contact = {}
@staticmethod
def _call(proxy, method_name):
try:
output = proxy.call_sync(
method_name, None, Gio.DBusCallFlags.NONE, -1, None)
if output:
return output[0]
except GLib.Error as error:
log.debug(error)
return None
def _error_callback(self, error):
log.debug(error)
# timeouts are non-critical
if str(error) != 'Timeout reached':
self.disconnect()
self._disconnected_cb()
def _new_service_callback(self, _browser, interface, protocol, name, stype,
domain, _flags):
log.debug('Found service %s in domain %s on %i.%i.',
name, domain, interface, protocol)
if not self.connected:
return
# synchronous resolving
try:
output = self._server.call_sync(
'ResolveService',
GLib.Variant('(iisssiu)', (interface, protocol, name, stype,
domain, Protocol.UNSPEC, 0)),
Gio.DBusCallFlags.NONE, -1, None)
self.service_resolved_callback(*output)
except GLib.Error as error:
log.debug('Error while resolving: %s', error)
def _remove_service_callback(self, _browser, interface, protocol, name,
_stype, domain, _flags):
log.debug('Service %s in domain %s on %i.%i disappeared.',
name, domain, interface, protocol)
if not self.connected:
return
if name == self.name:
return
for key in list(self._contacts.keys()):
val = self._contacts[key]
if val[Constant.BARE_NAME] == name:
# try to reduce instead of delete first
resolved_info = val[Constant.RESOLVED_INFO]
if len(resolved_info) > 1:
for i, _info in enumerate(resolved_info):
if resolved_info[i][ConstantRI.INTERFACE] == interface and resolved_info[i][ConstantRI.PROTOCOL] == protocol:
del self._contacts[key][Constant.RESOLVED_INFO][i]
# if still something left, don't remove
if len(self._contacts[key][Constant.RESOLVED_INFO]) > 1:
return
del self._contacts[key]
self._remove_service_cb(key)
return
def _new_service_type(self, interface, protocol, stype, domain, _flags):
# Are we already browsing this domain for this type?
if self._service_browser:
return
self._service_browser = Avahi.ServiceBrowser.new_full(
interface, protocol, stype, domain, 0)
self._avahi_client = Avahi.Client(flags=0,)
self._avahi_client.start()
con = self._service_browser.connect('new_service',
self._new_service_callback)
self._sb_connections.append(con)
con = self._service_browser.connect('removed_service',
self._remove_service_callback)
self._sb_connections.append(con)
con = self._service_browser.connect('failure', self._error_callback)
self._sb_connections.append(con)
self._service_browser.attach(self._avahi_client)
def _new_domain_callback(self, interface, protocol, domain, _flags):
if domain != 'local':
self._browse_domain(interface, protocol, domain)
@staticmethod
def txt_array_to_dict(txt_array):
txt_dict = {}
for array in txt_array:
item = bytes(array)
item = item.decode('utf-8')
item = item.split('=', 1)
if item[0] and (item[0] not in txt_dict):
if len(item) == 1:
txt_dict[item[0]] = None
else:
txt_dict[item[0]] = item[1]
return txt_dict
@staticmethod
def dict_to_txt_array(txt_dict):
array = []
for key, value in txt_dict.items():
item = '%s=%s' % (key, value)
item = item.encode('utf-8')
array.append(item)
return array
def service_resolved_callback(self, interface, protocol, name, _stype,
domain, host, aprotocol, address, port, txt,
_flags):
log.debug('Service data for service %s in domain %s on %i.%i:',
name, domain, interface, protocol)
log.debug('Host %s (%s), port %i, TXT data: %s',
host, address, port, self.txt_array_to_dict(txt))
if not self.connected:
return
bare_name = name
if name.find('@') == -1:
name = name + '@' + name
# we don't want to see ourselves in the list
if name != self.name:
resolved_info = [(interface, protocol, host,
aprotocol, address, int(port))]
if name in self._contacts:
# Decide whether to try to merge with existing resolved info:
old_name, old_domain, old_resolved_info, old_bare_name, _old_txt = self._contacts[name]
if name == old_name and domain == old_domain and bare_name == old_bare_name:
# Seems similar enough, try to merge resolved info:
for i, _info in enumerate(old_resolved_info):
# for now, keep a single record for each (interface, protocol) pair
#
# Note that, theoretically, we could both get IPv4 and
# IPv6 aprotocol responses via the same protocol,
# so this probably needs to be revised again.
if old_resolved_info[i][0:2] == (interface, protocol):
log.debug('Deleting resolved info for interface %s',
old_resolved_info[i])
del old_resolved_info[i]
break
resolved_info = resolved_info + old_resolved_info
log.debug('Collected resolved info is now: %s',
resolved_info)
self._contacts[name] = (name, domain, resolved_info, bare_name, txt)
self._new_service_cb(name)
else:
# remember data
# In case this is not our own record but of another
# gajim instance on the same machine,
# it will be used when we get a new name.
self._invalid_self_contact[name] = (
name,
domain,
(interface, protocol, host, aprotocol, address, int(port)),
bare_name,
txt)
def _service_resolved_all_callback(self, _interface, _protocol, name,
_stype, _domain, _host, _aprotocol,
_address, _port, txt, _flags):
if not self.connected:
return
if name.find('@') == -1:
name = name + '@' + name
# update TXT data only, as intended according to resolve_all comment
old_contact = self._contacts[name]
self._contacts[name] = old_contact[0:Constant.TXT] + (txt,) + old_contact[Constant.TXT+1:]
def _service_add_fail_callback(self, err):
log.debug('Error while adding service. %s', str(err))
if 'Local name collision' in str(err):
alternative_name = self._server.call_sync(
'GetAlternativeServiceName',
GLib.Variant('(s)', (self.username,)),
Gio.DBusCallFlags.NONE, -1, None)
self._name_conflict_cb(alternative_name[0])
return
self._error_cb(_('Error while adding service. %s') % str(err))
self.disconnect()
def _server_state_changed_callback(self, _connection, _sender_name,
_object_path, _interface_name,
_signal_name, parameters):
state, _ = parameters
log.debug('server state changed to %s', state)
if state == ServerState.RUNNING:
self._create_service()
elif state in (ServerState.COLLISION,
ServerState.REGISTERING):
self.disconnect()
if self._entrygroup:
self._call(self._entrygroup, 'Reset')
def _entrygroup_state_changed_callback(self, _connection, _sender_name,
_object_path, _interface_name,
_signal_name, parameters):
state, _ = parameters
# the name is already present, so recreate
if state == EntryGroup.COLLISION:
log.debug('zeroconf.py: local name collision')
self._service_add_fail_callback('Local name collision')
elif state == EntryGroup.FAILURE:
self.disconnect()
self._call(self._entrygroup, 'Reset')
log.debug('zeroconf.py: ENTRY_GROUP_FAILURE reached (that '
'should not happen)')
@staticmethod
def _replace_show(show):
if show in ['chat', 'online', '']:
return 'avail'
if show == 'xa':
return 'away'
return show
def avahi_txt(self):
return self.dict_to_txt_array(self.txt)
def _create_service(self):
try:
if not self._entrygroup:
# create an EntryGroup for publishing
object_path = self._server.call_sync(
'EntryGroupNew', None, Gio.DBusCallFlags.NONE, -1, None)
self._entrygroup = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None,
DBUS_NAME, *object_path, DBUS_INTERFACE_ENTRY_GROUP, None)
connection = self._entrygroup.get_connection()
subscription = connection.signal_subscribe(
DBUS_NAME, DBUS_INTERFACE_ENTRY_GROUP, 'StateChanged',
*object_path, None, Gio.DBusSignalFlags.NONE,
self._entrygroup_state_changed_callback)
self._connections[connection] = [subscription]
txt = {}
# remove empty keys
for key, val in self.txt.items():
if val:
txt[key] = val
txt['port.p2pj'] = self.port
txt['version'] = 1
txt['txtvers'] = 1
# replace gajim's show messages with compatible ones
if 'status' in self.txt:
txt['status'] = self._replace_show(self.txt['status'])
else:
txt['status'] = 'avail'
self.txt = txt
log.debug('Publishing service %s of type %s',
self.name, self.stype)
try:
self._entrygroup.call_sync(
'AddService',
GLib.Variant('(iiussssqaay)', (Interface.UNSPEC,
Protocol.UNSPEC, 0,
self.name, self.stype, '',
'', self.port,
self.avahi_txt())),
Gio.DBusCallFlags.NONE, -1, None)
except GLib.Error as error:
self._service_add_fail_callback(error)
return False
try:
self._entrygroup.call_sync('Commit', None,
Gio.DBusCallFlags.NONE, -1, None)
except GLib.Error as error:
pass
return True
except GLib.Error as error:
log.debug(error)
return False
def announce(self):
if not self.connected:
return False
state = self._server.call_sync(
'GetState', None, Gio.DBusCallFlags.NONE, -1, None)
if state[0] == ServerState.RUNNING:
if self._create_service():
self.announced = True
return True
return False
return None
def remove_announce(self):
if self.announced is False:
return False
if self._call(self._entrygroup, 'GetState') != EntryGroup.FAILURE:
self._call(self._entrygroup, 'Reset')
self._call(self._entrygroup, 'Free')
self._entrygroup = None
self.announced = False
return True
return False
def _browse_domain(self, interface, protocol, domain):
self._new_service_type(interface, protocol, self.stype, domain, '')
def _avahi_dbus_connect_cb(self, connection, sender_name, object_path,
interface_name, signal_name, parameters):
name, old_owner, new_owner = parameters
if name == DBUS_NAME:
if new_owner and not old_owner:
log.debug('We are connected to avahi-daemon')
else:
log.debug('Lost connection to avahi-daemon')
self.disconnect()
if self._disconnected_cb:
self._disconnected_cb()
def _connect_dbus(self):
try:
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None,
'org.freedesktop.DBus', '/org/freedesktop/DBus',
'org.freedesktop.DBus', None)
connection = proxy.get_connection()
subscription = connection.signal_subscribe(
'org.freedesktop.DBus', 'org.freedesktop.DBus',
'NameOwnerChanged', '/org/freedesktop/DBus', None,
Gio.DBusSignalFlags.NONE, self._avahi_dbus_connect_cb)
self._connections[connection] = [subscription]
except GLib.Error as error:
log.debug(error)
return False
else:
return True
def _connect_avahi(self):
if not self._connect_dbus():
return False
if self._server:
return True
try:
self._server = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None,
DBUS_NAME, '/', DBUS_INTERFACE_SERVER, None)
connection = self._server.get_connection()
subscription = connection.signal_subscribe(
DBUS_NAME, DBUS_INTERFACE_SERVER, 'StateChanged', '/', None,
Gio.DBusSignalFlags.NONE, self._server_state_changed_callback)
self._connections[connection] = [subscription]
except Exception as error:
# Avahi service is not present
self._server = None
log.debug(error)
return False
else:
return True
def connect(self):
self.name = self.username + '@' + self.host # service name
if not self._connect_avahi():
return False
self.connected = True
# start browsing
if self.domain is None:
# Explicitly browse .local
self._browse_domain(
Interface.UNSPEC, Protocol.UNSPEC, 'local')
# Browse for other browsable domains
object_path = self._server.call_sync(
'DomainBrowserNew',
GLib.Variant('(iisiu)', (Interface.UNSPEC, Protocol.UNSPEC, '',
DomainBrowser.BROWSE, 0)),
Gio.DBusCallFlags.NONE, -1, None)
self._domain_browser = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None, DBUS_NAME,
*object_path, DBUS_INTERFACE_DOMAIN_BROWSER, None)
connection = self._domain_browser.get_connection()
subscription = connection.signal_subscribe(
DBUS_NAME, DBUS_INTERFACE_DOMAIN_BROWSER, 'ItemNew',
*object_path, None, Gio.DBusSignalFlags.NONE,
self._new_domain_callback)
self._connections[connection] = [subscription]
subscription = connection.signal_subscribe(
DBUS_NAME, DBUS_INTERFACE_DOMAIN_BROWSER, 'Failure',
*object_path, None, Gio.DBusSignalFlags.NONE,
self._error_callback)
self._connections[connection].append(subscription)
else:
self._browse_domain(
Interface.UNSPEC, Protocol.UNSPEC, self.domain)
return True
def disconnect(self):
if self.connected:
self.connected = False
for connection, subscriptions in self._connections.items():
for subscription in subscriptions:
connection.signal_unsubscribe(subscription)
for con in self._sb_connections:
self._service_browser.disconnect(con)
if self._domain_browser:
self._call(self._domain_browser, 'Free')
self.remove_announce()
self._server = None
self._service_browser = None
self._domain_browser = None
# refresh txt data of all contacts manually (no callback available)
def resolve_all(self):
if not self.connected:
return False
for val in self._contacts.values():
# get txt data from last recorded resolved info
# TODO: Better try to get it from last IPv6 mDNS, then last IPv4?
ri = val[Constant.RESOLVED_INFO][0]
output = self._server.call_sync(
'ResolveService',
GLib.Variant('(iisssiu)', (ri[ConstantRI.INTERFACE],
ri[ConstantRI.PROTOCOL],
val[Constant.BARE_NAME],
self.stype, val[Constant.DOMAIN],
Protocol.UNSPEC,
0)),
Gio.DBusCallFlags.NONE,
-1,
None
)
self._service_resolved_all_callback(*output)
return True
def get_contacts(self):
return self._contacts
def get_contact(self, jid):
if not jid in self._contacts:
return None
return self._contacts[jid]
def update_txt(self, show=None):
if show:
self.txt['status'] = self._replace_show(show)
txt = self.avahi_txt()
if self.connected and self._entrygroup:
try:
self._entrygroup.call_sync(
'UpdateServiceTxt',
GLib.Variant('(iiusssaay)', (Interface.UNSPEC,
Protocol.UNSPEC, 0, self.name,
self.stype, '', txt)),
Gio.DBusCallFlags.NONE, -1, None)
except GLib.Error as error:
self._error_callback(error)
return False
return True
return False

View file

@ -0,0 +1,58 @@
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from enum import IntEnum
DBUS_NAME = "org.freedesktop.Avahi"
DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
DBUS_INTERFACE_DOMAIN_BROWSER = DBUS_NAME + ".DomainBrowser"
class ServerState(IntEnum):
INVALID = 0
REGISTERING = 1
RUNNING = 2
COLLISION = 3
FAILURE = 4
class EntryGroup(IntEnum):
UNCOMMITTED = 0
REGISTERING = 1
ESTABLISHED = 2
COLLISION = 3
FAILURE = 4
class DomainBrowser(IntEnum):
BROWSE = 0
BROWSE_DEFAULT = 1
REGISTER = 2
REGISTER_DEFAULT = 3
BROWSE_LEGACY = 4
class Protocol(IntEnum):
UNSPEC = -1
INET = 0
INET6 = 1
class Interface(IntEnum):
UNSPEC = -1

View file

@ -0,0 +1,458 @@
# Copyright (C) 2006 Stefan Bethge <stefan@lanpartei.de>
# Copyright (C) 2006 Philipp Hörist <philipp@hoerist.com>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import select
import re
from gajim.common.i18n import _
from gajim.common.zeroconf.zeroconf import Constant
log = logging.getLogger('gajim.c.z.zeroconf_bonjour')
try:
from pybonjour import kDNSServiceErr_NoError
from pybonjour import kDNSServiceErr_ServiceNotRunning
from pybonjour import kDNSServiceErr_NameConflict
from pybonjour import kDNSServiceInterfaceIndexAny
from pybonjour import kDNSServiceType_TXT
from pybonjour import kDNSServiceFlagsAdd
from pybonjour import kDNSServiceFlagsNoAutoRename
from pybonjour import BonjourError
from pybonjour import TXTRecord
from pybonjour import DNSServiceUpdateRecord
from pybonjour import DNSServiceResolve
from pybonjour import DNSServiceProcessResult
from pybonjour import DNSServiceGetAddrInfo
from pybonjour import DNSServiceQueryRecord
from pybonjour import DNSServiceBrowse
from pybonjour import DNSServiceRegister
except ImportError:
pass
resolve_timeout = 1
class Zeroconf:
def __init__(self, new_service_cb, remove_service_cb, name_conflict_cb,
_disconnected_cb, error_cb, name, host, port):
self.stype = '_presence._tcp'
self.port = port # listening port that gets announced
self.username = name
self.host = host
self.txt = {} # service data
self.name = None
self.connected = False
self.announced = False
# XXX these CBs should be set to None when we destroy the object
# (go offline), because they create a circular reference
self._new_service_cb = new_service_cb
self._remove_service_cb = remove_service_cb
self._name_conflict_cb = name_conflict_cb
self._error_cb = error_cb
self._service_sdref = None
self._browse_sdref = None
self._contacts = {} # all current local contacts with data
self._invalid_self_contact = {}
self._resolved_hosts = {}
self._resolved = []
self._queried = []
def _browse_callback(self, _sdref, flags, interface, error_code,
service_name, regtype, reply_domain):
log.debug('Found service %s in domain %s on %i(type: %s).',
service_name, reply_domain, interface, regtype)
if not self.connected:
return
if error_code != kDNSServiceErr_NoError:
log.debug('Error in browse_callback: %s', str(error_code))
return
if not flags & kDNSServiceFlagsAdd:
self._remove_service_callback(service_name)
return
try:
# asynchronous resolving
resolve_sdref = None
resolve_sdref = DNSServiceResolve(
0, interface, service_name,
regtype, reply_domain, self._service_resolved_callback)
while not self._resolved:
ready = select.select([resolve_sdref], [], [], resolve_timeout)
if resolve_sdref not in ready[0]:
log.info('Resolve timed out')
break
DNSServiceProcessResult(resolve_sdref)
else:
self._resolved.pop()
except BonjourError as error:
log.info('Error when resolving DNS: %s', error)
finally:
if resolve_sdref:
resolve_sdref.close()
def _remove_service_callback(self, name):
log.info('Service %s disappeared.', name)
if not self.connected:
return
if name != self.name:
for key in list(self._contacts.keys()):
if self._contacts[key][Constant.NAME] == name:
del self._contacts[key]
self._remove_service_cb(key)
return
@staticmethod
def txt_array_to_dict(txt):
if not isinstance(txt, TXTRecord):
txt = TXTRecord.parse(txt)
return dict((v[0], v[1]) for v in txt)
@staticmethod
def _parse_name(fullname):
log.debug('Parse name: %s', fullname)
# TODO: do proper decoding...
escaping = {r'\.': '.',
r'\032': ' ',
r'\064': '@',
}
# Split on '.' but do not split on '\.'
result = re.split(r'(?<!\\\\)\.', fullname)
name = result[0]
protocol, domain = result[2:4]
# Replace the escaped values
for src, trg in escaping.items():
name = name.replace(src, trg)
bare_name = name
if '@' not in name:
name = name + '@' + name
log.debug('End parse: %s %s %s %s',
name, bare_name, protocol, domain)
return name, bare_name, protocol, domain
def _query_txt_callback(self, _sdref, _flags, _interface, error_code,
hosttarget, _rrtype, _rrclass, rdata, _ttl):
if error_code != kDNSServiceErr_NoError:
log.error('Error in query_record_callback: %s', str(error_code))
return
name = self._parse_name(hosttarget)[0]
if name != self.name:
# update TXT data only, as intended according to
# resolve_all comment
old_contact = self._contacts[name]
self._contacts[name] = old_contact[0:Constant.TXT] + (rdata,) + old_contact[Constant.TXT+1:]
log.debug(self._contacts[name])
self._queried.append(True)
def _getaddrinfo_callback(self, _sdref, _flags, interface, error_code,
hosttarget, address, _ttl):
if error_code != kDNSServiceErr_NoError:
log.error('Error in getaddrinfo_callback: %s', str(error_code))
return
fullname, port, txt_record = self._resolved_hosts[hosttarget]
txt = TXTRecord.parse(txt_record)
ip = address[1]
name, bare_name, protocol, domain = self._parse_name(fullname)
log.info('Service data for service %s on %i:',
fullname, interface)
log.info('Host %s, ip %s, port %i, TXT data: %s',
hosttarget, ip, port, txt)
if not self.connected:
return
# we don't want to see ourselves in the list
if name != self.name:
resolved_info = [(interface, protocol, hosttarget,
fullname, ip, port)]
self._contacts[name] = (name, domain, resolved_info,
bare_name, txt_record)
self._new_service_cb(name)
else:
# remember data
# In case this is not our own record but of another
# gajim instance on the same machine,
# it will be used when we get a new name.
self._invalid_self_contact[name] = \
(name, domain,
(interface, protocol, hosttarget, fullname, ip, port),
bare_name, txt_record)
self._queried.append(True)
def _service_resolved_callback(self, _sdref, _flags, interface,
error_code, fullname, hosttarget, port,
txt_record):
if error_code != kDNSServiceErr_NoError:
log.error('Error in service_resolved_callback: %s', str(error_code))
return
self._resolved_hosts[hosttarget] = (fullname, port, txt_record)
try:
getaddrinfo_sdref = \
DNSServiceGetAddrInfo(
interfaceIndex=interface,
hostname=hosttarget,
callBack=self._getaddrinfo_callback)
while not self._queried:
ready = select.select(
[getaddrinfo_sdref], [], [], resolve_timeout)
if getaddrinfo_sdref not in ready[0]:
log.warning('GetAddrInfo timed out')
break
DNSServiceProcessResult(getaddrinfo_sdref)
else:
self._queried.pop()
except BonjourError as error:
if error.error_code == kDNSServiceErr_ServiceNotRunning:
log.info('Service not running')
else:
self._error_cb(_('Error while adding service. %s') % error)
finally:
if getaddrinfo_sdref:
getaddrinfo_sdref.close()
self._resolved.append(True)
def service_added_callback(self, _sdref, _flags, error_code,
_name, _regtype, _domain):
if error_code == kDNSServiceErr_NoError:
log.info('Service successfully added')
elif error_code == kDNSServiceErr_NameConflict:
log.error('Error while adding service. %s', error_code)
self._name_conflict_cb(self._get_alternativ_name(self.username))
else:
error = _('Error while adding service. %s') % str(error_code)
self._error_cb(error)
@staticmethod
def _get_alternativ_name(name):
if name[-2] == '-':
try:
number = int(name[-1])
except Exception:
return '%s-1' % name
return '%s-%s' % (name[:-2], number + 1)
return '%s-1' % name
@staticmethod
def _replace_show(show):
if show in ['chat', 'online', '']:
return 'avail'
if show == 'xa':
return 'away'
return show
def _create_service(self):
txt = {}
# remove empty keys
for key, val in self.txt.items():
if val:
txt[key] = val
txt['port.p2pj'] = self.port
txt['version'] = 1
txt['txtvers'] = 1
# replace gajim's show messages with compatible ones
if 'status' in self.txt:
txt['status'] = self._replace_show(self.txt['status'])
else:
txt['status'] = 'avail'
self.txt = txt
try:
self._service_sdref = DNSServiceRegister(
flags=kDNSServiceFlagsNoAutoRename,
name=self.name,
regtype=self.stype,
port=self.port,
txtRecord=TXTRecord(self.txt, strict=True),
callBack=self.service_added_callback)
log.info('Publishing service %s of type %s', self.name, self.stype)
ready = select.select([self._service_sdref], [], [])
if self._service_sdref in ready[0]:
DNSServiceProcessResult(self._service_sdref)
except BonjourError as error:
if error.errorCode == kDNSServiceErr_ServiceNotRunning:
log.info('Service not running')
else:
self._error_cb(_('Error while adding service. %s') % error)
self.disconnect()
def announce(self):
if not self.connected:
return False
self._create_service()
self.announced = True
return True
def remove_announce(self):
if not self.announced:
return False
if self._service_sdref is None:
return False
try:
self._service_sdref.close()
self.announced = False
return True
except BonjourError as error:
log.error('Error when removing announce: %s', error)
return False
def connect(self):
self.name = self.username + '@' + self.host # service name
self.connected = True
# start browsing
if self.browse_domain():
return True
self.disconnect()
return False
def disconnect(self):
if self.connected:
self.connected = False
if self._browse_sdref is not None:
self._browse_sdref.close()
self._browse_sdref = None
self.remove_announce()
def browse_domain(self, domain=None):
try:
self._browse_sdref = DNSServiceBrowse(
regtype=self.stype,
domain=domain,
callBack=self._browse_callback)
log.info('Starting to browse .local')
return True
except BonjourError as error:
if error.errorCode == kDNSServiceErr_ServiceNotRunning:
log.info('Service not running')
else:
log.error('Error while browsing for services. %s', error)
return False
def browse_loop(self):
try:
ready = select.select([self._browse_sdref], [], [], 0)
if self._browse_sdref in ready[0]:
DNSServiceProcessResult(self._browse_sdref)
except BonjourError as error:
if error.errorCode == kDNSServiceErr_ServiceNotRunning:
log.info('Service not running')
return False
log.error('Error while browsing for services. %s', error)
return True
# resolve_all() is called every X seconds and queries for new clients
# and monitors TXT records for changed status
def resolve_all(self):
if not self.connected:
return False
# for now put here as this is synchronous
if not self.browse_loop():
return False
# Monitor TXT Records with DNSServiceQueryRecord because
# its more efficient (see pybonjour documentation)
for val in self._contacts.values():
bare_name = val[Constant.RESOLVED_INFO][0][Constant.BARE_NAME]
try:
query_sdref = None
query_sdref = \
DNSServiceQueryRecord(
interfaceIndex=kDNSServiceInterfaceIndexAny,
fullname=bare_name,
rrtype=kDNSServiceType_TXT,
callBack=self._query_txt_callback)
while not self._queried:
ready = select.select(
[query_sdref], [], [], resolve_timeout)
if query_sdref not in ready[0]:
log.info('Query record timed out')
break
DNSServiceProcessResult(query_sdref)
else:
self._queried.pop()
except BonjourError as error:
if error.errorCode == kDNSServiceErr_ServiceNotRunning:
log.info('Service not running')
return False
log.error('Error in query for TXT records. %s', error)
finally:
if query_sdref:
query_sdref.close()
return True
def get_contacts(self):
return self._contacts
def get_contact(self, jid):
if jid not in self._contacts:
return None
return self._contacts[jid]
def update_txt(self, show=None):
if show:
self.txt['status'] = self._replace_show(show)
txt = TXTRecord(self.txt, strict=True)
try:
DNSServiceUpdateRecord(self._service_sdref, None, 0, txt)
except BonjourError as error:
log.error('Error when updating TXT Record: %s', error)
return False
return True