Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits:
-
dab91c3d
by Philipp Hörist at 2019-11-24T06:40:07Z
-
f28ff1df
by Philipp Hörist at 2019-11-24T21:10:10Z
-
dc337d8c
by Philipp Hörist at 2019-11-26T22:07:48Z
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:
| ... | ... | @@ -553,4 +553,21 @@ CHATSTATES = [ |
| 553 | 553 |
'gone',
|
| 554 | 554 |
'composing',
|
| 555 | 555 |
'paused'
|
| 556 |
-]
|
|
| \ No newline at end of file | ||
| 556 |
+]
|
|
| 557 |
+ |
|
| 558 |
+REGISTER_FIELDS = [
|
|
| 559 |
+ 'username',
|
|
| 560 |
+ 'nick',
|
|
| 561 |
+ 'password',
|
|
| 562 |
+ 'name',
|
|
| 563 |
+ 'first',
|
|
| 564 |
+ 'last',
|
|
| 565 |
+ 'email',
|
|
| 566 |
+ 'address',
|
|
| 567 |
+ 'city',
|
|
| 568 |
+ 'state',
|
|
| 569 |
+ 'zip',
|
|
| 570 |
+ 'phone',
|
|
| 571 |
+ 'url',
|
|
| 572 |
+ 'date',
|
|
| 573 |
+]
|
| ... | ... | @@ -19,7 +19,6 @@ import logging |
| 19 | 19 |
import hashlib
|
| 20 | 20 |
|
| 21 | 21 |
from nbxmpp.protocol import NS_BOB
|
| 22 |
-from nbxmpp.protocol import InvalidStanza
|
|
| 23 | 22 |
from nbxmpp.structs import BobData
|
| 24 | 23 |
from nbxmpp.util import b64decode
|
| 25 | 24 |
|
| ... | ... | @@ -29,7 +28,7 @@ log = logging.getLogger('nbxmpp.m.bob') |
| 29 | 28 |
def parse_bob_data(stanza):
|
| 30 | 29 |
data_node = stanza.getTag('data', namespace=NS_BOB)
|
| 31 | 30 |
if data_node is None:
|
| 32 |
- return
|
|
| 31 |
+ return None
|
|
| 33 | 32 |
|
| 34 | 33 |
cid = data_node.getAttr('cid')
|
| 35 | 34 |
type_ = data_node.getAttr('type')
|
| ... | ... | @@ -39,42 +38,42 @@ def parse_bob_data(stanza): |
| 39 | 38 |
max_age = int(max_age)
|
| 40 | 39 |
except Exception:
|
| 41 | 40 |
log.exception(stanza)
|
| 42 |
- raise InvalidStanza
|
|
| 41 |
+ return None
|
|
| 43 | 42 |
|
| 44 | 43 |
if cid is None or type_ is None:
|
| 45 | 44 |
log.warning('Invalid data node (no cid or type attr): %s', stanza)
|
| 46 |
- raise InvalidStanza
|
|
| 45 |
+ return None
|
|
| 47 | 46 |
|
| 48 | 47 |
try:
|
| 49 | 48 |
algo_hash = cid.split('@')[0]
|
| 50 | 49 |
algo, hash_ = algo_hash.split('+')
|
| 51 | 50 |
except Exception:
|
| 52 | 51 |
log.exception('Invalid cid: %s', stanza)
|
| 53 |
- raise InvalidStanza
|
|
| 52 |
+ return None
|
|
| 54 | 53 |
|
| 55 | 54 |
bob_data = data_node.getData()
|
| 56 | 55 |
if not bob_data:
|
| 57 | 56 |
log.warning('No bob data found: %s', stanza)
|
| 58 |
- raise InvalidStanza
|
|
| 57 |
+ return None
|
|
| 59 | 58 |
|
| 60 | 59 |
try:
|
| 61 | 60 |
bob_data = b64decode(bob_data, return_type=bytes)
|
| 62 | 61 |
except Exception:
|
| 63 | 62 |
log.warning('Unable to decode data')
|
| 64 | 63 |
log.exception(stanza)
|
| 65 |
- raise InvalidStanza
|
|
| 64 |
+ return None
|
|
| 66 | 65 |
|
| 67 | 66 |
try:
|
| 68 | 67 |
sha = hashlib.new(algo)
|
| 69 | 68 |
except ValueError as error:
|
| 70 | 69 |
log.warning(stanza)
|
| 71 | 70 |
log.warning(error)
|
| 72 |
- raise InvalidStanza
|
|
| 71 |
+ return None
|
|
| 73 | 72 |
|
| 74 | 73 |
sha.update(bob_data)
|
| 75 | 74 |
if sha.hexdigest() != hash_:
|
| 76 | 75 |
log.warning('Invalid hash: %s', stanza)
|
| 77 |
- raise InvalidStanza
|
|
| 76 |
+ return None
|
|
| 78 | 77 |
|
| 79 | 78 |
return BobData(algo=algo,
|
| 80 | 79 |
hash_=hash_,
|
| ... | ... | @@ -19,7 +19,6 @@ import logging |
| 19 | 19 |
|
| 20 | 20 |
from nbxmpp.protocol import NS_CAPTCHA
|
| 21 | 21 |
from nbxmpp.protocol import NS_DATA
|
| 22 |
-from nbxmpp.protocol import InvalidStanza
|
|
| 23 | 22 |
from nbxmpp.structs import StanzaHandler
|
| 24 | 23 |
from nbxmpp.structs import CaptchaData
|
| 25 | 24 |
from nbxmpp.modules.dataforms import extend_form
|
| ... | ... | @@ -51,10 +50,7 @@ class Captcha: |
| 51 | 50 |
return
|
| 52 | 51 |
|
| 53 | 52 |
form = extend_form(node=data_form)
|
| 54 |
- try:
|
|
| 55 |
- bob_data = parse_bob_data(stanza)
|
|
| 56 |
- except InvalidStanza:
|
|
| 57 |
- bob_data = None
|
|
| 53 |
+ bob_data = parse_bob_data(stanza)
|
|
| 58 | 54 |
|
| 59 | 55 |
properties.captcha = CaptchaData(form=form,
|
| 60 | 56 |
bob_data=bob_data)
|
| ... | ... | @@ -17,13 +17,23 @@ |
| 17 | 17 |
|
| 18 | 18 |
import logging
|
| 19 | 19 |
|
| 20 |
+from nbxmpp.protocol import NS_X_OOB
|
|
| 21 |
+from nbxmpp.protocol import NS_DATA
|
|
| 20 | 22 |
from nbxmpp.protocol import NS_REGISTER
|
| 21 | 23 |
from nbxmpp.protocol import Iq
|
| 22 | 24 |
from nbxmpp.protocol import isResultNode
|
| 23 | 25 |
from nbxmpp.structs import CommonResult
|
| 26 |
+from nbxmpp.structs import RegisterData
|
|
| 27 |
+from nbxmpp.structs import ChangePasswordResult
|
|
| 24 | 28 |
from nbxmpp.util import call_on_response
|
| 25 | 29 |
from nbxmpp.util import callback
|
| 26 | 30 |
from nbxmpp.util import raise_error
|
| 31 |
+from nbxmpp.util import get_form
|
|
| 32 |
+from nbxmpp.const import REGISTER_FIELDS
|
|
| 33 |
+from nbxmpp.modules.bits_of_binary import parse_bob_data
|
|
| 34 |
+from nbxmpp.modules.dataforms import extend_form
|
|
| 35 |
+from nbxmpp.modules.dataforms import create_field
|
|
| 36 |
+from nbxmpp.modules.dataforms import SimpleDataForm
|
|
| 27 | 37 |
|
| 28 | 38 |
log = logging.getLogger('nbxmpp.m.register')
|
| 29 | 39 |
|
| ... | ... | @@ -42,8 +52,103 @@ class Register: |
| 42 | 52 |
query.addChild('remove')
|
| 43 | 53 |
return iq
|
| 44 | 54 |
|
| 55 |
+ @call_on_response('_on_register_form')
|
|
| 56 |
+ def request_register_form(self, jid=None):
|
|
| 57 |
+ if jid is None:
|
|
| 58 |
+ jid = self._client.Server
|
|
| 59 |
+ return Iq('get', NS_REGISTER, to=jid)
|
|
| 60 |
+ |
|
| 61 |
+ @callback
|
|
| 62 |
+ def _on_register_form(self, stanza):
|
|
| 63 |
+ query = stanza.getQuery()
|
|
| 64 |
+ |
|
| 65 |
+ instructions = query.getTagData('instructions') or None
|
|
| 66 |
+ |
|
| 67 |
+ return RegisterData(instructions=instructions,
|
|
| 68 |
+ form=self._parse_form(stanza),
|
|
| 69 |
+ fields_form=self._parse_fields_form(query),
|
|
| 70 |
+ oob_url=self._parse_oob_url(query),
|
|
| 71 |
+ bob_data=parse_bob_data(query))
|
|
| 72 |
+ |
|
| 45 | 73 |
@callback
|
| 46 | 74 |
def _default_response(self, stanza):
|
| 47 | 75 |
if not isResultNode(stanza):
|
| 48 | 76 |
return raise_error(log.info, stanza)
|
| 49 | 77 |
return CommonResult(jid=stanza.getFrom())
|
| 78 |
+ |
|
| 79 |
+ @staticmethod
|
|
| 80 |
+ def _parse_oob_url(query):
|
|
| 81 |
+ oob = query.getTag('x', namespace=NS_X_OOB)
|
|
| 82 |
+ if oob is not None:
|
|
| 83 |
+ return oob.getTagData('url') or None
|
|
| 84 |
+ return None
|
|
| 85 |
+ |
|
| 86 |
+ @staticmethod
|
|
| 87 |
+ def _parse_form(stanza):
|
|
| 88 |
+ form = stanza.getQuery().getTag('x', namespace=NS_DATA)
|
|
| 89 |
+ if form is None:
|
|
| 90 |
+ return None
|
|
| 91 |
+ |
|
| 92 |
+ form = extend_form(node=form)
|
|
| 93 |
+ field = form.vars.get('FORM_TYPE')
|
|
| 94 |
+ if field is None:
|
|
| 95 |
+ log.warning('No FORM_TYPE found')
|
|
| 96 |
+ log.warning(stanza)
|
|
| 97 |
+ return None
|
|
| 98 |
+ |
|
| 99 |
+ # Invalid urn:xmpp:captcha used by ejabberd
|
|
| 100 |
+ # See https://github.com/processone/ejabberd/issues/3093
|
|
| 101 |
+ if field.value in ('jabber:iq:register', 'urn:xmpp:captcha'):
|
|
| 102 |
+ return form
|
|
| 103 |
+ return None
|
|
| 104 |
+ |
|
| 105 |
+ @staticmethod
|
|
| 106 |
+ def _parse_fields_form(query):
|
|
| 107 |
+ fields = []
|
|
| 108 |
+ for field in query.getChildren():
|
|
| 109 |
+ field_name = field.getName()
|
|
| 110 |
+ if field_name not in REGISTER_FIELDS:
|
|
| 111 |
+ continue
|
|
| 112 |
+ |
|
| 113 |
+ required = field_name in ('username', 'password')
|
|
| 114 |
+ typ = 'text-single' if field_name != 'password' else 'text-private'
|
|
| 115 |
+ fields.append(create_field(typ=typ,
|
|
| 116 |
+ var=field_name,
|
|
| 117 |
+ required=required))
|
|
| 118 |
+ |
|
| 119 |
+ if not fields:
|
|
| 120 |
+ return None
|
|
| 121 |
+ |
|
| 122 |
+ fields.append(create_field(typ='hidden', var='fakeform'))
|
|
| 123 |
+ return SimpleDataForm(type_='form', fields=fields)
|
|
| 124 |
+ |
|
| 125 |
+ @call_on_response('_on_password_change')
|
|
| 126 |
+ def change_password(self, password):
|
|
| 127 |
+ hostname = self._client.get_bound_jid().getDomain()
|
|
| 128 |
+ username = self._client.get_bound_jid().getNode()
|
|
| 129 |
+ iq = Iq('set', NS_REGISTER, to=hostname)
|
|
| 130 |
+ query = iq.getQuery()
|
|
| 131 |
+ query.setTagData('username', username)
|
|
| 132 |
+ query.setTagData('password', password)
|
|
| 133 |
+ return iq
|
|
| 134 |
+ |
|
| 135 |
+ @callback
|
|
| 136 |
+ def _on_password_change(self, stanza):
|
|
| 137 |
+ if isResultNode(stanza):
|
|
| 138 |
+ return ChangePasswordResult(successful=True)
|
|
| 139 |
+ |
|
| 140 |
+ if stanza.getQuery() is None:
|
|
| 141 |
+ return raise_error(log.info, stanza)
|
|
| 142 |
+ |
|
| 143 |
+ form = get_form(stanza.getQuery(),
|
|
| 144 |
+ 'jabber:iq:register:changepassword')
|
|
| 145 |
+ if form is None:
|
|
| 146 |
+ return raise_error(log.info, stanza)
|
|
| 147 |
+ return ChangePasswordResult(successful=False, form=form)
|
|
| 148 |
+ |
|
| 149 |
+ @call_on_response('_default_response')
|
|
| 150 |
+ def change_password_with_form(self, form):
|
|
| 151 |
+ hostname = self._client.get_bound_jid().getDomain()
|
|
| 152 |
+ iq = Iq('set', NS_REGISTER, to=hostname)
|
|
| 153 |
+ iq.setQueryPayload(form)
|
|
| 154 |
+ return iq
|
| ... | ... | @@ -131,6 +131,10 @@ CorrectionData = namedtuple('CorrectionData', 'id') |
| 131 | 131 |
DisplayMarking = namedtuple('DisplayMarking', 'label fgcolor bgcolor')
|
| 132 | 132 |
SecurityLabel = namedtuple('SecurityLabel', 'displaymarking')
|
| 133 | 133 |
|
| 134 |
+RegisterData = namedtuple('RegisterData', 'instructions form fields_form oob_url bob_data')
|
|
| 135 |
+ |
|
| 136 |
+ChangePasswordResult = namedtuple('ChangePasswordResult', 'successful form')
|
|
| 137 |
+ChangePasswordResult.__new__.__defaults__ = (None,)
|
|
| 134 | 138 |
|
| 135 | 139 |
class DiscoInfo(namedtuple('DiscoInfo', 'stanza identities features dataforms timestamp')):
|
| 136 | 140 |
|
| ... | ... | @@ -321,3 +321,19 @@ def compute_caps_hash(info, compare=True): |
| 321 | 321 |
|
| 322 | 322 |
def generate_id():
|
| 323 | 323 |
return str(uuid.uuid4())
|
| 324 |
+ |
|
| 325 |
+ |
|
| 326 |
+def get_form(stanza, form_type):
|
|
| 327 |
+ forms = stanza.getTags('x', namespace=NS_DATA)
|
|
| 328 |
+ if not forms:
|
|
| 329 |
+ return None
|
|
| 330 |
+ |
|
| 331 |
+ for form in forms:
|
|
| 332 |
+ form = extend_form(node=form)
|
|
| 333 |
+ field = form.vars.get('FORM_TYPE')
|
|
| 334 |
+ if field is None:
|
|
| 335 |
+ continue
|
|
| 336 |
+ |
|
| 337 |
+ if field.value == form_type:
|
|
| 338 |
+ return form
|
|
| 339 |
+ return None
|