Commit 112c06bc authored by Philipp Hörist's avatar Philipp Hörist
Browse files

Inital

parent 22aaefb2
Pipeline #8576 failed with stages
in 22 seconds
......@@ -106,7 +106,7 @@ python-nbxmpp 0.6.0 (25 September 2017)
* Add new getOriginID/SetOriginID method for Messages
* Add new getJid() method for Protocol
* getTagAttr() accepts now a namespace argument
* find_tag_attr() accepts now a namespace argument
* Add new `protocol` argument for getTag()
* Add new XEP Namespaces
......
import gi
from .protocol import *
gi.require_version('Soup', '2.4')
__version__ = "3.0.0-dev1"
......@@ -15,9 +15,15 @@
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Iterator
from typing import NamedTuple
from typing import Optional
import logging
from collections import namedtuple
from nbxmpp.structs import ProxyData
from nbxmpp.util import Observable
from nbxmpp.resolver import GioResolver
from nbxmpp.const import ConnectionType
......@@ -27,10 +33,14 @@ from nbxmpp.const import ConnectionProtocol
log = logging.getLogger('nbxmpp.addresses')
class ServerAddress(namedtuple('ServerAddress', 'domain service host uri '
'protocol type proxy')):
__slots__ = []
class ServerAddress(NamedTuple):
domain: str
service: Optional[str]
host: Optional[str]
uri: Optional[str]
protocol: ConnectionProtocol
type: ConnectionType
proxy: Optional[ProxyData]
@property
def is_service(self):
......@@ -56,7 +66,7 @@ class ServerAddresses(Observable):
'''
def __init__(self, domain):
def __init__(self, domain: str):
Observable.__init__(self, log)
self._domain = domain
......@@ -64,7 +74,7 @@ class ServerAddresses(Observable):
self._proxy = None
self._is_resolved = False
self._addresses = [
self._addresses: list[ServerAddress] = [
ServerAddress(domain=self._domain,
service='xmpps-client',
host=None,
......@@ -109,11 +119,11 @@ class ServerAddresses(Observable):
]
@property
def domain(self):
def domain(self) -> str:
return self._domain
@property
def is_resolved(self):
def is_resolved(self) -> bool:
return self._is_resolved
def resolve(self):
......@@ -136,7 +146,9 @@ class ServerAddresses(Observable):
def cancel_resolve(self):
self.remove_subscriptions()
def set_custom_host(self, address):
def set_custom_host(self, address: Optional[tuple[str,
ConnectionProtocol,
ConnectionType]]):
# Set a custom host, overwrites all other addresses
self._custom_host = address
if address is None:
......@@ -158,10 +170,10 @@ class ServerAddresses(Observable):
type=type_,
proxy=None)]
def set_proxy(self, proxy):
def set_proxy(self, proxy: Optional[ProxyData]):
self._proxy = proxy
def _on_alternatives_result(self, uri):
def _on_alternatives_result(self, uri: Optional[str]):
if uri is None:
self._on_request_resolved()
return
......@@ -182,6 +194,7 @@ class ServerAddresses(Observable):
protocol=ConnectionProtocol.WEBSOCKET,
type=type_,
proxy=None)
self._addresses.append(addr)
self._on_request_resolved()
......@@ -192,8 +205,8 @@ class ServerAddresses(Observable):
self.remove_subscriptions()
def get_next_address(self,
allowed_types,
allowed_protocols):
allowed_types: list[ConnectionType],
allowed_protocols: list[ConnectionProtocol]) -> Iterator[ServerAddress]:
'''
Selects next address
'''
......@@ -210,7 +223,7 @@ class ServerAddresses(Observable):
raise NoMoreAddresses
def _assure_proxy(self, addr):
def _assure_proxy(self, addr: ServerAddress):
if self._proxy is None:
return addr
......@@ -219,17 +232,22 @@ class ServerAddresses(Observable):
return addr
def _filter_allowed(self, addresses, allowed_types, allowed_protocols):
def _filter_allowed(self,
addresses: list[ServerAddress],
allowed_types: list[ConnectionType],
allowed_protocols: list[ConnectionProtocol]) -> list[ServerAddress]:
if self._proxy is not None:
addresses = filter(lambda addr: addr.host is not None, addresses)
addresses = list(filter(lambda addr: addr.host is not None,
addresses))
addresses = filter(lambda addr: addr.type in allowed_types,
addresses)
addresses = filter(lambda addr: addr.protocol in allowed_protocols,
addresses)
addresses = list(filter(lambda addr: addr.type in allowed_types,
addresses))
addresses = list(filter(lambda addr: addr.protocol in allowed_protocols,
addresses))
return addresses
def __str__(self):
def __str__(self) -> str:
addresses = self._addresses + self._fallback_addresses
return '\n'.join([str(addr) for addr in addresses])
......
......@@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Optional
import os
import hmac
import binascii
......@@ -23,13 +27,13 @@ import hashlib
from hashlib import pbkdf2_hmac
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.protocol import SASL_ERROR_CONDITIONS
from nbxmpp.protocol import SASL_AUTH_MECHS
from nbxmpp.const import SASL_ERROR_CONDITIONS
from nbxmpp.const import SASL_AUTH_MECHS
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.util import LogAdapter
from nbxmpp.const import StreamState
from nbxmpp.builder import E
log = logging.getLogger('nbxmpp.auth')
......@@ -42,6 +46,19 @@ except (ImportError, OSError) as error:
GSSAPI_AVAILABLE = False
def make_sasl_element(name: str,
mechanism: Optional[str] = None,
payload: Optional[str] = None):
if mechanism is None:
element = E(name, namespace=Namespace.XMPP_SASL)
else:
element = E(name, namespace=Namespace.XMPP_SASL, mechanism=mechanism)
element.text = payload
return element
class SASL:
"""
Implements SASL authentication.
......@@ -70,13 +87,13 @@ class SASL:
return self._password
def delegate(self, stanza):
if stanza.getNamespace() != Namespace.SASL:
if stanza.namespace != Namespace.XMPP_SASL:
return
if stanza.getName() == 'challenge':
if stanza.localname == 'challenge':
self._on_challenge(stanza)
elif stanza.getName() == 'failure':
elif stanza.localname == 'failure':
self._on_failure(stanza)
elif stanza.getName() == 'success':
elif stanza.localname == 'success':
self._on_success(stanza)
def start_auth(self, features):
......@@ -171,7 +188,7 @@ class SASL:
def _on_challenge(self, stanza):
try:
self._method.response(stanza.getData())
self._method.response(stanza.text or '')
except AttributeError:
self._log.info('Mechanism has no response method')
self._abort_auth()
......@@ -182,7 +199,7 @@ class SASL:
def _on_success(self, stanza):
self._log.info('Successfully authenticated with remote server')
try:
self._method.success(stanza.getData())
self._method.success(stanza.text or '')
except AttributeError:
pass
except AuthFail as error:
......@@ -193,11 +210,11 @@ class SASL:
self._on_sasl_finished(True, None, None)
def _on_failure(self, stanza):
text = stanza.getTagData('text')
text = stanza.find_tag_text('text')
reason = 'not-authorized'
childs = stanza.getChildren()
childs = stanza.get_children()
for child in childs:
name = child.getName()
name = child.localname
if name == 'text':
continue
if name in SASL_ERROR_CONDITIONS:
......@@ -208,8 +225,8 @@ class SASL:
self._abort_auth(reason, text)
def _abort_auth(self, reason='malformed-request', text=None):
node = Node('abort', attrs={'xmlns': Namespace.SASL})
self._client.send_nonza(node)
element = make_sasl_element('abort')
self._client.send_nonza(element)
self._on_sasl_finished(False, reason, text)
def _on_sasl_finished(self, successful, reason, text=None):
......@@ -229,10 +246,8 @@ class PLAIN:
def initiate(self, username, password):
payload = b64encode('\x00%s\x00%s' % (username, password))
node = Node('auth',
attrs={'xmlns': Namespace.SASL, 'mechanism': 'PLAIN'},
payload=[payload])
self._client.send_nonza(node)
element = make_sasl_element('auth', mechanism='PLAIN', payload=payload)
self._client.send_nonza(element)
class EXTERNAL:
......@@ -244,10 +259,10 @@ class EXTERNAL:
def initiate(self, username, server):
payload = b64encode('%s@%s' % (username, server))
node = Node('auth',
attrs={'xmlns': Namespace.SASL, 'mechanism': 'EXTERNAL'},
payload=[payload])
self._client.send_nonza(node)
element = make_sasl_element('auth',
mechanism='EXTERNAL',
payload=payload)
self._client.send_nonza(element)
class ANONYMOUS:
......@@ -258,9 +273,8 @@ class ANONYMOUS:
self._client = client
def initiate(self):
node = Node('auth', attrs={'xmlns': Namespace.SASL,
'mechanism': 'ANONYMOUS'})
self._client.send_nonza(node)
element = make_sasl_element('auth', mechanism='ANONYMOUS')
self._client.send_nonza(element)
class GSSAPI:
......@@ -282,10 +296,11 @@ class GSSAPI:
token = self.ctx.step()
except (gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError) as e:
raise AuthFail(e)
node = Node('auth',
attrs={'xmlns': Namespace.SASL, 'mechanism': 'GSSAPI'},
payload=b64encode(token))
self._client.send_nonza(node)
element = make_sasl_element('auth',
mechanism='GSSAPI',
payload=b64encode(token))
self._client.send_nonza(element)
def response(self, server_message, *args, **kwargs):
server_message = b64decode(server_message)
......@@ -299,11 +314,10 @@ class GSSAPI:
output_token = self.ctx.wrap(data, False).message
except (gssapi.exceptions.GeneralError, gssapi.raw.misc.GSSError) as e:
raise AuthFail(e)
response = b64encode(output_token)
node = Node('response',
attrs={'xmlns': Namespace.SASL},
payload=response)
self._client.send_nonza(node)
payload = b64encode(output_token)
element = make_sasl_element('response', payload=payload)
self._client.send_nonza(element)
class SCRAM:
......@@ -342,12 +356,11 @@ class SCRAM:
client_first_message = '%s%s' % (self._channel_binding,
self._client_first_message_bare)
payload = b64encode(client_first_message)
node = Node('auth',
attrs={'xmlns': Namespace.SASL,
'mechanism': self._mechanism},
payload=[payload])
self._client.send_nonza(node)
element = make_sasl_element('auth',
mechanism=self._mechanism,
payload=b64encode(client_first_message))
self._client.send_nonza(element)
def response(self, server_first_message):
server_first_message = b64decode(server_first_message).decode()
......@@ -390,11 +403,10 @@ class SCRAM:
server_key = self._hmac(salted_password, 'Server Key')
self._server_signature = self._hmac(server_key, auth_message)
payload = b64encode(client_finale_message)
node = Node('response',
attrs={'xmlns': Namespace.SASL},
payload=[payload])
self._client.send_nonza(node)
element = make_sasl_element('response',
payload=b64encode(client_finale_message))
self._client.send_nonza(element)
def success(self, server_last_message):
server_last_message = b64decode(server_last_message).decode()
......
# This file is part of nbxmpp.
#
# This program 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; either version 3
# of the License, or (at your option) any later version.
#
# This program 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 this program; If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Optional
from typing import Union
from typing import cast
from lxml import etree
from nbxmpp.const import IqType, MessageType, PresenceType
from nbxmpp.lookups import ElementLookup
from nbxmpp.elements import Base
from nbxmpp.elements import create_nsmap_and_tag
from nbxmpp.jid import JID
from nbxmpp.namespaces import Namespace
from nbxmpp import types
_element_parser = etree.XMLParser()
_element_parser.set_element_class_lookup(ElementLookup)
def E(tag: str,
text: Optional[str] = None,
namespace: Optional[str] = None,
**attrib: str) -> Base:
tag, nsmap = create_nsmap_and_tag(tag, namespace)
element = cast(Base, _element_parser.makeelement(tag,
nsmap=nsmap,
attrib=attrib))
if text is not None:
element.text = text
return element
def Message(to: Union[str, JID],
type: Optional[str] = None,
id: Optional[str] = None) -> types.Message:
message = cast(types.Message, E('message', namespace='jabber:client'))
if isinstance(to, str):
to = JID.from_string(to)
message.set_to(str(to))
if type is not None:
MessageType(type)
message.set('type', type)
if id is not None:
message.set('id', id)
return message
def Iq(to: Optional[Union[str, JID]] = None,
type: Optional[str] = 'get',
id: Optional[str] = None) -> types.Iq:
iq = cast(types.Iq, E('iq', namespace='jabber:client'))
IqType(type)
iq.set('type', type)
if isinstance(to, str):
to = JID.from_string(to)
iq.set_to(str(to))
if id is not None:
iq.set('id', id)
return iq
def Presence(to: Optional[Union[str, JID]] = None,
type: Optional[str] = None,
id: Optional[str] = None,
priority: Optional[int] = None,
show: Optional[str] = None,
status: Optional[str] = None,
nickname: Optional[str] = None,
idle_time: Optional[str] = None,
signed: Optional[str] = None,
muc_join: Optional[bool] = False,
muc_history: Optional[str] = None,
muc_password: Optional[str] = None,
caps: Optional[dict[str, str]] = None) -> types.Presence:
presence = cast(types.Presence, E('presence', namespace='jabber:client'))
if type is not None:
PresenceType(type)
presence.set('type', type)
if to is not None:
if isinstance(to, str):
to = JID.from_string(to)
presence.set_to(str(to))
if id is not None:
presence.set('id', id)
if priority is not None:
if priority not in range(-128, 128):
raise ValueError('invalid priority: %s' % priority)
presence.add_tag_text('priority', str(priority))
if show is not None:
if show not in ('chat', 'away', 'xa', 'dnd'):
raise ValueError('invalid show value: %s' % show)
presence.add_tag_text('show', show)
if status is not None:
presence.add_tag_text('status', status)
if caps is not None and type != 'unavailable':
presence.add_tag('c', namespace=Namespace.CAPS, **caps)
if nickname is not None:
presence.add_tag_text('nick', nickname, namespace=Namespace.NICK)
if idle_time is not None:
presence.add_tag('idle', namespace=Namespace.IDLE, since=idle_time)
if signed is not None:
presence.add_tag_text('x', signed, namespace=Namespace.SIGNED)
if muc_join or muc_history is not None or muc_password is not None:
muc_x = presence.add_tag('x', namespace=Namespace.MUC)
if muc_history is not None:
muc_x.add_tag_text('history', muc_history)
if muc_password is not None:
muc_x.add_tag_text('password', muc_password)
return presence
def DataForm(type: str) -> types.DataForm:
dataform = E('x', namespace=Namespace.DATA)
dataform.set_type(type)
return dataform
def parse(data: str) -> Base:
return cast(Base, etree.fromstring(data, _element_parser))
## c14n.py
##
## Copyright (C) 2007-2008 Brendan Taylor <whateley AT 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/>.
##
"""
XML canonicalisation methods (for XEP-0116)
"""
def c14n(node, is_buggy):
s = "<" + node.name
if node.namespace:
if not node.parent or node.parent.namespace != node.namespace:
s += ' xmlns="%s"' % node.namespace
sorted_attrs = sorted(node.attrs.keys())
for key in sorted_attrs:
if not is_buggy and key == 'xmlns':
continue
val = str(node.attrs[key])
# like XMLescape() but with whitespace and without &gt;
s += ' %s="%s"' % (key, normalise_attr(val))
s += ">"
cnt = 0
if node.kids:
for a in node.kids:
if (len(node.data)-1) >= cnt:
s = s + normalise_text(node.data[cnt])
s = s + c14n(a, is_buggy)
cnt += 1
if (len(node.data)-1) >= cnt:
s = s + normalise_text(node.data[cnt])
if not node.kids and s.endswith('>'):
s=s[:-1]+' />'
else:
s = s + "</" + node.name + ">"
return s
def normalise_attr(val):
return val.replace('&', '&amp;').replace('<', '&lt;').replace('"', '&quot;').replace('\t', '&#x9;').replace('\n', '&#xA;').replace('\r', '&#xD;')
def normalise_text(val):
return val.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\r', '&#xD;')
This diff is collapsed.
......@@ -15,13 +15,18 @@
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any, Optional, cast
import logging
from gi.repository import Gio
from nbxmpp.const import TCPState
from nbxmpp.const import ConnectionType, TCPState
from nbxmpp.util import Observable
from nbxmpp.util import LogAdapter
from nbxmpp.addresses import ServerAddress
log = logging.getLogger('nbxmpp.connection')
......@@ -40,11 +45,11 @@ class Connection(Observable):
disconnected
'''