Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: dab91c3d by Philipp Hörist at 2019-11-24T06:40:07Z BOB: Make parse_bob_data() return None on error
- - - - - f28ff1df by Philipp Hörist at 2019-11-24T21:10:10Z Register: Add request_register_form()
- - - - - dc337d8c by Philipp Hörist at 2019-11-26T22:07:48Z Register: Add change_password()
- - - - -
6 changed files:
- nbxmpp/const.py - nbxmpp/modules/bits_of_binary.py - nbxmpp/modules/captcha.py - nbxmpp/modules/register.py - nbxmpp/structs.py - nbxmpp/util.py
Changes:
===================================== nbxmpp/const.py ===================================== @@ -553,4 +553,21 @@ CHATSTATES = [ 'gone', 'composing', 'paused' -] \ No newline at end of file +] + +REGISTER_FIELDS = [ + 'username', + 'nick', + 'password', + 'name', + 'first', + 'last', + 'email', + 'address', + 'city', + 'state', + 'zip', + 'phone', + 'url', + 'date', +]
===================================== nbxmpp/modules/bits_of_binary.py ===================================== @@ -19,7 +19,6 @@ import logging import hashlib
from nbxmpp.protocol import NS_BOB -from nbxmpp.protocol import InvalidStanza from nbxmpp.structs import BobData from nbxmpp.util import b64decode
@@ -29,7 +28,7 @@ log = logging.getLogger('nbxmpp.m.bob') def parse_bob_data(stanza): data_node = stanza.getTag('data', namespace=NS_BOB) if data_node is None: - return + return None
cid = data_node.getAttr('cid') type_ = data_node.getAttr('type') @@ -39,42 +38,42 @@ def parse_bob_data(stanza): max_age = int(max_age) except Exception: log.exception(stanza) - raise InvalidStanza + return None
if cid is None or type_ is None: log.warning('Invalid data node (no cid or type attr): %s', stanza) - raise InvalidStanza + return None
try: algo_hash = cid.split('@')[0] algo, hash_ = algo_hash.split('+') except Exception: log.exception('Invalid cid: %s', stanza) - raise InvalidStanza + return None
bob_data = data_node.getData() if not bob_data: log.warning('No bob data found: %s', stanza) - raise InvalidStanza + return None
try: bob_data = b64decode(bob_data, return_type=bytes) except Exception: log.warning('Unable to decode data') log.exception(stanza) - raise InvalidStanza + return None
try: sha = hashlib.new(algo) except ValueError as error: log.warning(stanza) log.warning(error) - raise InvalidStanza + return None
sha.update(bob_data) if sha.hexdigest() != hash_: log.warning('Invalid hash: %s', stanza) - raise InvalidStanza + return None
return BobData(algo=algo, hash_=hash_,
===================================== nbxmpp/modules/captcha.py ===================================== @@ -19,7 +19,6 @@ import logging
from nbxmpp.protocol import NS_CAPTCHA from nbxmpp.protocol import NS_DATA -from nbxmpp.protocol import InvalidStanza from nbxmpp.structs import StanzaHandler from nbxmpp.structs import CaptchaData from nbxmpp.modules.dataforms import extend_form @@ -51,10 +50,7 @@ class Captcha: return
form = extend_form(node=data_form) - try: - bob_data = parse_bob_data(stanza) - except InvalidStanza: - bob_data = None + bob_data = parse_bob_data(stanza)
properties.captcha = CaptchaData(form=form, bob_data=bob_data)
===================================== nbxmpp/modules/register.py ===================================== @@ -17,13 +17,23 @@
import logging
+from nbxmpp.protocol import NS_X_OOB +from nbxmpp.protocol import NS_DATA from nbxmpp.protocol import NS_REGISTER from nbxmpp.protocol import Iq from nbxmpp.protocol import isResultNode from nbxmpp.structs import CommonResult +from nbxmpp.structs import RegisterData +from nbxmpp.structs import ChangePasswordResult from nbxmpp.util import call_on_response from nbxmpp.util import callback from nbxmpp.util import raise_error +from nbxmpp.util import get_form +from nbxmpp.const import REGISTER_FIELDS +from nbxmpp.modules.bits_of_binary import parse_bob_data +from nbxmpp.modules.dataforms import extend_form +from nbxmpp.modules.dataforms import create_field +from nbxmpp.modules.dataforms import SimpleDataForm
log = logging.getLogger('nbxmpp.m.register')
@@ -42,8 +52,103 @@ class Register: query.addChild('remove') return iq
+ @call_on_response('_on_register_form') + def request_register_form(self, jid=None): + if jid is None: + jid = self._client.Server + return Iq('get', NS_REGISTER, to=jid) + + @callback + def _on_register_form(self, stanza): + query = stanza.getQuery() + + instructions = query.getTagData('instructions') or None + + return RegisterData(instructions=instructions, + form=self._parse_form(stanza), + fields_form=self._parse_fields_form(query), + oob_url=self._parse_oob_url(query), + bob_data=parse_bob_data(query)) + @callback def _default_response(self, stanza): if not isResultNode(stanza): return raise_error(log.info, stanza) return CommonResult(jid=stanza.getFrom()) + + @staticmethod + def _parse_oob_url(query): + oob = query.getTag('x', namespace=NS_X_OOB) + if oob is not None: + return oob.getTagData('url') or None + return None + + @staticmethod + def _parse_form(stanza): + form = stanza.getQuery().getTag('x', namespace=NS_DATA) + if form is None: + return None + + form = extend_form(node=form) + field = form.vars.get('FORM_TYPE') + if field is None: + log.warning('No FORM_TYPE found') + log.warning(stanza) + return None + + # Invalid urn:xmpp:captcha used by ejabberd + # See https://github.com/processone/ejabberd/issues/3093 + if field.value in ('jabber:iq:register', 'urn:xmpp:captcha'): + return form + return None + + @staticmethod + def _parse_fields_form(query): + fields = [] + for field in query.getChildren(): + field_name = field.getName() + if field_name not in REGISTER_FIELDS: + continue + + required = field_name in ('username', 'password') + typ = 'text-single' if field_name != 'password' else 'text-private' + fields.append(create_field(typ=typ, + var=field_name, + required=required)) + + if not fields: + return None + + fields.append(create_field(typ='hidden', var='fakeform')) + return SimpleDataForm(type_='form', fields=fields) + + @call_on_response('_on_password_change') + def change_password(self, password): + hostname = self._client.get_bound_jid().getDomain() + username = self._client.get_bound_jid().getNode() + iq = Iq('set', NS_REGISTER, to=hostname) + query = iq.getQuery() + query.setTagData('username', username) + query.setTagData('password', password) + return iq + + @callback + def _on_password_change(self, stanza): + if isResultNode(stanza): + return ChangePasswordResult(successful=True) + + if stanza.getQuery() is None: + return raise_error(log.info, stanza) + + form = get_form(stanza.getQuery(), + 'jabber:iq:register:changepassword') + if form is None: + return raise_error(log.info, stanza) + return ChangePasswordResult(successful=False, form=form) + + @call_on_response('_default_response') + def change_password_with_form(self, form): + hostname = self._client.get_bound_jid().getDomain() + iq = Iq('set', NS_REGISTER, to=hostname) + iq.setQueryPayload(form) + return iq
===================================== nbxmpp/structs.py ===================================== @@ -131,6 +131,10 @@ CorrectionData = namedtuple('CorrectionData', 'id') DisplayMarking = namedtuple('DisplayMarking', 'label fgcolor bgcolor') SecurityLabel = namedtuple('SecurityLabel', 'displaymarking')
+RegisterData = namedtuple('RegisterData', 'instructions form fields_form oob_url bob_data') + +ChangePasswordResult = namedtuple('ChangePasswordResult', 'successful form') +ChangePasswordResult.__new__.__defaults__ = (None,)
class DiscoInfo(namedtuple('DiscoInfo', 'stanza identities features dataforms timestamp')):
===================================== nbxmpp/util.py ===================================== @@ -321,3 +321,19 @@ def compute_caps_hash(info, compare=True):
def generate_id(): return str(uuid.uuid4()) + + +def get_form(stanza, form_type): + forms = stanza.getTags('x', namespace=NS_DATA) + if not forms: + return None + + for form in forms: + form = extend_form(node=form) + field = form.vars.get('FORM_TYPE') + if field is None: + continue + + if field.value == form_type: + return form + return None
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/compare/34a0ed853e7a948adae6758ae3...