9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz
This commit is contained in:
parent
a5b3822651
commit
4c1b226bff
1045 changed files with 753037 additions and 18 deletions
0
gajim/common/zeroconf/__init__.py
Normal file
0
gajim/common/zeroconf/__init__.py
Normal file
857
gajim/common/zeroconf/client_zeroconf.py
Normal file
857
gajim/common/zeroconf/client_zeroconf.py
Normal 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()
|
||||
# don’t 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:
|
||||
# Can’t 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)
|
127
gajim/common/zeroconf/connection_handlers_zeroconf.py
Normal file
127
gajim/common/zeroconf/connection_handlers_zeroconf.py
Normal 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')
|
||||
|
||||
# Don’t 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))
|
450
gajim/common/zeroconf/connection_zeroconf.py
Normal file
450
gajim/common/zeroconf/connection_zeroconf.py
Normal 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
|
160
gajim/common/zeroconf/roster_zeroconf.py
Normal file
160
gajim/common/zeroconf/roster_zeroconf.py
Normal 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()
|
63
gajim/common/zeroconf/zeroconf.py
Normal file
63
gajim/common/zeroconf/zeroconf.py
Normal 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
|
552
gajim/common/zeroconf/zeroconf_avahi.py
Normal file
552
gajim/common/zeroconf/zeroconf_avahi.py
Normal 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
|
58
gajim/common/zeroconf/zeroconf_avahi_const.py
Normal file
58
gajim/common/zeroconf/zeroconf_avahi_const.py
Normal 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
|
458
gajim/common/zeroconf/zeroconf_bonjour.py
Normal file
458
gajim/common/zeroconf/zeroconf_bonjour.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue