Compare commits
9 Commits
slix-1.2.2
...
WIP
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6034df0a78 | ||
|
|
df4012e66d | ||
|
|
c372f3071a | ||
|
|
829c8b27b6 | ||
|
|
fb3ac78bf9 | ||
|
|
ffd9436e5c | ||
|
|
bbb1344d79 | ||
|
|
457785b286 | ||
|
|
4847f834bd |
8
.gitlab-ci.yml
Normal file
8
.gitlab-ci.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
test:
|
||||
tags:
|
||||
- docker
|
||||
image: ubuntu:latest
|
||||
script:
|
||||
- apt update
|
||||
- apt install -y python3 cython3
|
||||
- ./run_tests.py
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
from slixmpp import ClientXMPP
|
||||
|
||||
from optparse import OptionParser
|
||||
from argparse import ArgumentParser
|
||||
import logging
|
||||
import getpass
|
||||
|
||||
@@ -58,40 +58,40 @@ if __name__ == '__main__':
|
||||
# ./http_over_xmpp.py -J <jid> -P <pwd> -i <ip> -p <port> [-v]
|
||||
#
|
||||
|
||||
parser = OptionParser()
|
||||
parser = ArgumentParser()
|
||||
|
||||
# Output verbosity options.
|
||||
parser.add_option(
|
||||
parser.add_argument(
|
||||
'-v', '--verbose', help='set logging to DEBUG', action='store_const',
|
||||
dest='loglevel', const=logging.DEBUG, default=logging.ERROR
|
||||
)
|
||||
|
||||
# JID and password options.
|
||||
parser.add_option('-J', '--jid', dest='jid', help='JID')
|
||||
parser.add_option('-P', '--password', dest='password', help='Password')
|
||||
parser.add_argument('-J', '--jid', dest='jid', help='JID')
|
||||
parser.add_argument('-P', '--password', dest='password', help='Password')
|
||||
|
||||
# XMPP server ip and port options.
|
||||
parser.add_option(
|
||||
parser.add_argument(
|
||||
'-i', '--ipaddr', dest='ipaddr',
|
||||
help='IP Address of the XMPP server', default=None
|
||||
)
|
||||
parser.add_option(
|
||||
parser.add_argument(
|
||||
'-p', '--port', dest='port',
|
||||
help='Port of the XMPP server', default=None
|
||||
)
|
||||
|
||||
opts, args = parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
logging.basicConfig(level=args.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = input('Username: ')
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass('Password: ')
|
||||
if args.jid is None:
|
||||
args.jid = input('Username: ')
|
||||
if args.password is None:
|
||||
args.password = getpass.getpass('Password: ')
|
||||
|
||||
xmpp = HTTPOverXMPPClient(opts.jid, opts.password)
|
||||
xmpp = HTTPOverXMPPClient(args.jid, args.password)
|
||||
xmpp.connect()
|
||||
xmpp.process()
|
||||
|
||||
|
||||
23
setup.py
23
setup.py
@@ -9,7 +9,7 @@
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from subprocess import call, DEVNULL
|
||||
from subprocess import call, DEVNULL, check_output, CalledProcessError
|
||||
from tempfile import TemporaryFile
|
||||
try:
|
||||
from setuptools import setup
|
||||
@@ -35,18 +35,31 @@ CLASSIFIERS = [
|
||||
|
||||
packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')]
|
||||
|
||||
def check_include(header):
|
||||
command = [os.environ.get('CC', 'cc'), '-E', '-']
|
||||
def check_include(library_name, header):
|
||||
command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name]
|
||||
try:
|
||||
cflags = check_output(command).decode('utf-8').split()
|
||||
except FileNotFoundError:
|
||||
print('pkg-config not found.')
|
||||
return False
|
||||
except CalledProcessError:
|
||||
# pkg-config already prints the missing libraries on stderr.
|
||||
return False
|
||||
command = [os.environ.get('CC', 'cc')] + cflags + ['-E', '-']
|
||||
with TemporaryFile('w+') as c_file:
|
||||
c_file.write('#include <%s>' % header)
|
||||
c_file.seek(0)
|
||||
try:
|
||||
return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0
|
||||
except FileNotFoundError:
|
||||
print('%s headers not found.' % library_name)
|
||||
return False
|
||||
|
||||
HAS_PYTHON_HEADERS = check_include('python3', 'Python.h')
|
||||
HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h')
|
||||
|
||||
ext_modules = None
|
||||
if check_include('stringprep.h'):
|
||||
if HAS_PYTHON_HEADERS and HAS_STRINGPREP_HEADERS:
|
||||
try:
|
||||
from Cython.Build import cythonize
|
||||
except ImportError:
|
||||
@@ -54,7 +67,7 @@ if check_include('stringprep.h'):
|
||||
else:
|
||||
ext_modules = cythonize('slixmpp/stringprep.pyx')
|
||||
else:
|
||||
print('libidn-dev not found, falling back to the slow stringprep module.')
|
||||
print('Falling back to the slow stringprep module.')
|
||||
|
||||
setup(
|
||||
name="slixmpp",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from slixmpp.jid import JID
|
||||
from slixmpp.stanza import StreamFeatures
|
||||
from slixmpp.basexmpp import BaseXMPP
|
||||
from slixmpp.exceptions import XMPPError
|
||||
@@ -110,7 +111,13 @@ class ClientXMPP(BaseXMPP):
|
||||
self._handle_stream_features))
|
||||
def roster_push_filter(iq):
|
||||
from_ = iq['from']
|
||||
if from_ and from_ != self.boundjid.bare:
|
||||
if from_ and from_ != JID('') and from_ != self.boundjid.bare:
|
||||
reply = iq.reply()
|
||||
reply['type'] = 'error'
|
||||
reply['error']['type'] = 'cancel'
|
||||
reply['error']['code'] = 503
|
||||
reply['error']['condition'] = 'service-unavailable'
|
||||
reply.send()
|
||||
return
|
||||
self.event('roster_update', iq)
|
||||
self.register_handler(
|
||||
|
||||
15
slixmpp/plugins/xep_0380/__init__.py
Normal file
15
slixmpp/plugins/xep_0380/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp.plugins.base import register_plugin
|
||||
|
||||
from slixmpp.plugins.xep_0380.stanza import Encryption
|
||||
from slixmpp.plugins.xep_0380.eme import XEP_0380
|
||||
|
||||
|
||||
register_plugin(XEP_0380)
|
||||
69
slixmpp/plugins/xep_0380/eme.py
Normal file
69
slixmpp/plugins/xep_0380/eme.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import slixmpp
|
||||
from slixmpp.stanza import Message
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.plugins.xep_0380 import stanza, Encryption
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XEP_0380(BasePlugin):
|
||||
|
||||
"""
|
||||
XEP-0380: Explicit Message Encryption
|
||||
"""
|
||||
|
||||
name = 'xep_0380'
|
||||
description = 'XEP-0380: Explicit Message Encryption'
|
||||
dependencies = {'xep_0030'}
|
||||
default_config = {
|
||||
'template': 'This message is encrypted with {name} ({namespace})',
|
||||
}
|
||||
|
||||
mechanisms = {
|
||||
'jabber:x:encrypted': 'Legacy OpenPGP',
|
||||
'urn:xmpp:ox:0': 'OpenPGP for XMPP',
|
||||
'urn:xmpp:otr:0': 'OTR',
|
||||
'eu.siacs.conversations.axolotl': 'Legacy OMEMO',
|
||||
'urn:xmpp:omemo:0': 'OMEMO',
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp.register_handler(
|
||||
Callback('Explicit Message Encryption',
|
||||
StanzaPath('message/eme'),
|
||||
self._handle_eme))
|
||||
|
||||
register_stanza_plugin(Message, Encryption)
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp.remove_handler('Chat State')
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp.plugin['xep_0030'].add_feature(Encryption.namespace)
|
||||
|
||||
def has_eme(self, msg):
|
||||
return msg.xml.find('{%s}encryption' % Encryption.namespace) is not None
|
||||
|
||||
def replace_body_with_eme(self, msg):
|
||||
eme = msg['eme']
|
||||
namespace = eme['namespace']
|
||||
name = self.mechanisms[namespace] if namespace in self.mechanisms else eme['name']
|
||||
body = self.config['template'].format(name=name, namespace=namespace)
|
||||
msg['body'] = body
|
||||
|
||||
def _handle_eme(self, msg):
|
||||
self.xmpp.event('message_encryption', msg)
|
||||
16
slixmpp/plugins/xep_0380/stanza.py
Normal file
16
slixmpp/plugins/xep_0380/stanza.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp.xmlstream import ElementBase
|
||||
|
||||
|
||||
class Encryption(ElementBase):
|
||||
name = 'encryption'
|
||||
namespace = 'urn:xmpp:eme:0'
|
||||
plugin_attrib = 'eme'
|
||||
interfaces = {'namespace', 'name'}
|
||||
@@ -9,5 +9,5 @@
|
||||
# We don't want to have to import the entire library
|
||||
# just to get the version info for setup.py
|
||||
|
||||
__version__ = '1.2.2'
|
||||
__version_info__ = (1, 2, 2)
|
||||
__version__ = '1.2.3'
|
||||
__version_info__ = (1, 2, 3)
|
||||
|
||||
@@ -19,7 +19,7 @@ import ssl
|
||||
import weakref
|
||||
import uuid
|
||||
|
||||
import xml.etree.ElementTree
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from slixmpp.xmlstream.asyncio import asyncio
|
||||
from slixmpp.xmlstream import tostring, highlight
|
||||
@@ -339,7 +339,7 @@ class XMLStream(asyncio.BaseProtocol):
|
||||
"""
|
||||
self.xml_depth = 0
|
||||
self.xml_root = None
|
||||
self.parser = xml.etree.ElementTree.XMLPullParser(("start", "end"))
|
||||
self.parser = ET.XMLPullParser(("start", "end"))
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Called when the TCP connection has been established with the server
|
||||
@@ -359,32 +359,46 @@ class XMLStream(asyncio.BaseProtocol):
|
||||
the stream is opened, etc).
|
||||
"""
|
||||
self.parser.feed(data)
|
||||
for event, xml in self.parser.read_events():
|
||||
if event == 'start':
|
||||
if self.xml_depth == 0:
|
||||
# We have received the start of the root element.
|
||||
self.xml_root = xml
|
||||
log.debug('[33;1mRECV[0m: %s', highlight(tostring(self.xml_root, xmlns=self.default_ns,
|
||||
stream=self,
|
||||
top_level=True,
|
||||
open_only=True)))
|
||||
self.start_stream_handler(self.xml_root)
|
||||
self.xml_depth += 1
|
||||
if event == 'end':
|
||||
self.xml_depth -= 1
|
||||
if self.xml_depth == 0:
|
||||
# The stream's root element has closed,
|
||||
# terminating the stream.
|
||||
log.debug("End of stream received")
|
||||
self.abort()
|
||||
elif self.xml_depth == 1:
|
||||
# A stanza is an XML element that is a direct child of
|
||||
# the root element, hence the check of depth == 1
|
||||
self._spawn_event(xml)
|
||||
if self.xml_root is not None:
|
||||
# Keep the root element empty of children to
|
||||
# save on memory use.
|
||||
self.xml_root.clear()
|
||||
try:
|
||||
for event, xml in self.parser.read_events():
|
||||
if event == 'start':
|
||||
if self.xml_depth == 0:
|
||||
# We have received the start of the root element.
|
||||
self.xml_root = xml
|
||||
log.debug('[33;1mRECV[0m: %s',
|
||||
highlight(tostring(self.xml_root,
|
||||
xmlns=self.default_ns,
|
||||
stream=self,
|
||||
top_level=True,
|
||||
open_only=True)))
|
||||
self.start_stream_handler(self.xml_root)
|
||||
self.xml_depth += 1
|
||||
if event == 'end':
|
||||
self.xml_depth -= 1
|
||||
if self.xml_depth == 0:
|
||||
# The stream's root element has closed,
|
||||
# terminating the stream.
|
||||
log.debug("End of stream received")
|
||||
self.abort()
|
||||
elif self.xml_depth == 1:
|
||||
# A stanza is an XML element that is a direct child of
|
||||
# the root element, hence the check of depth == 1
|
||||
self._spawn_event(xml)
|
||||
if self.xml_root is not None:
|
||||
# Keep the root element empty of children to
|
||||
# save on memory use.
|
||||
self.xml_root.clear()
|
||||
except ET.ParseError:
|
||||
log.error('Parse error: %r', data)
|
||||
|
||||
# Due to cyclic dependencies, this can’t be imported at the module
|
||||
# level.
|
||||
from slixmpp.stanza.stream_error import StreamError
|
||||
error = StreamError()
|
||||
error['condition'] = 'not-well-formed'
|
||||
error['text'] = 'Server sent: %r' % data
|
||||
self.send(error)
|
||||
self.disconnect()
|
||||
|
||||
def is_connected(self):
|
||||
return self.transport is not None
|
||||
|
||||
37
tests/test_stanza_xep_0380.py
Normal file
37
tests/test_stanza_xep_0380.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import unittest
|
||||
from slixmpp import Message
|
||||
from slixmpp.test import SlixTest
|
||||
import slixmpp.plugins.xep_0380 as xep_0380
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
|
||||
|
||||
class TestEME(SlixTest):
|
||||
|
||||
def setUp(self):
|
||||
register_stanza_plugin(Message, xep_0380.stanza.Encryption)
|
||||
|
||||
def testCreateEME(self):
|
||||
"""Testing creating EME."""
|
||||
|
||||
xmlstring = """
|
||||
<message>
|
||||
<encryption xmlns="urn:xmpp:eme:0" namespace="%s"%s />
|
||||
</message>
|
||||
"""
|
||||
|
||||
msg = self.Message()
|
||||
self.check(msg, "<message />")
|
||||
|
||||
msg['eme']['namespace'] = 'urn:xmpp:otr:0'
|
||||
self.check(msg, xmlstring % ('urn:xmpp:otr:0', ''))
|
||||
|
||||
msg['eme']['namespace'] = 'urn:xmpp:openpgp:0'
|
||||
self.check(msg, xmlstring % ('urn:xmpp:openpgp:0', ''))
|
||||
|
||||
msg['eme']['name'] = 'OX'
|
||||
self.check(msg, xmlstring % ('urn:xmpp:openpgp:0', ' name="OX"'))
|
||||
|
||||
del msg['eme']
|
||||
self.check(msg, "<message />")
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestEME)
|
||||
Reference in New Issue
Block a user