summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKeith Wall <kwall@apache.org>2014-09-25 14:40:31 +0000
committerKeith Wall <kwall@apache.org>2014-09-25 14:40:31 +0000
commit0336eaa7a0a6c414ff292e96871f3dc6d5a95204 (patch)
tree4e917fb21a33482d19da20331dee0ff22b574554
parent6c4b6c2d0f7739996fa5120c897ec1de800f7107 (diff)
downloadqpid-python-0336eaa7a0a6c414ff292e96871f3dc6d5a95204.tar.gz
QPID-6116: [Python Client 08..091] Add ability to negotiate SASL mechanism and add pure python implementations for SCRAM/CRAM/PLAIN mechanisms
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk/qpid@1627554 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--python/qpid/client.py33
-rw-r--r--python/qpid/saslmech/__init__.py22
-rw-r--r--python/qpid/saslmech/amqplain.py31
-rw-r--r--python/qpid/saslmech/anonymous.py23
-rw-r--r--python/qpid/saslmech/cram_md5.py30
-rw-r--r--python/qpid/saslmech/cram_md5_hex.py33
-rw-r--r--python/qpid/saslmech/external.py23
-rw-r--r--python/qpid/saslmech/finder.py63
-rw-r--r--python/qpid/saslmech/plain.py31
-rw-r--r--python/qpid/saslmech/sasl.py41
-rw-r--r--python/qpid/saslmech/scram.py94
-rw-r--r--python/qpid/saslmech/scram_sha_1.py27
-rw-r--r--python/qpid/saslmech/scram_sha_256.py25
-rw-r--r--python/qpid/testlib.py5
-rw-r--r--python/qpid/tests/__init__.py1
-rw-r--r--python/qpid/tests/saslmech/__init__.py19
-rw-r--r--python/qpid/tests/saslmech/finder.py62
-rw-r--r--python/qpid/tests/saslmech/my_sasl.py22
-rw-r--r--python/qpid/tests/saslmech/my_sasl2.py25
-rw-r--r--tests/src/py/qpid_tests/broker_0_9/echo.py2
20 files changed, 605 insertions, 7 deletions
diff --git a/python/qpid/client.py b/python/qpid/client.py
index 9380594973..b0ce5d9009 100644
--- a/python/qpid/client.py
+++ b/python/qpid/client.py
@@ -30,6 +30,8 @@ from connection08 import Connection, Frame, connect
from spec08 import load
from queue import Queue
from reference import ReferenceId, References
+from saslmech.finder import get_sasl_mechanism
+from saslmech.sasl import SaslException
class Client:
@@ -48,6 +50,7 @@ class Client:
self.mechanism = None
self.response = None
self.locale = None
+ self.sasl = None
self.vhost = vhost
if self.vhost == None:
@@ -77,12 +80,17 @@ class Client:
self.lock.release()
return q
- def start(self, response, mechanism="AMQPLAIN", locale="en_US", tune_params=None, client_properties=None, connection_options=None):
+ def start(self, response=None, mechanism=None, locale="en_US", tune_params=None,
+ username=None, password=None,
+ client_properties=None, connection_options=None, sasl_options = None):
self.mechanism = mechanism
self.response = response
+ self.username = username
+ self.password = password
self.locale = locale
self.tune_params = tune_params
self.client_properties=get_client_properties_with_defaults(provided_client_properties=client_properties)
+ self.sasl_options = sasl_options
self.socket = connect(self.host, self.port, connection_options)
self.conn = Connection(self.socket, self.spec)
self.peer = Peer(self.conn, ClientDelegate(self), Session)
@@ -127,11 +135,32 @@ class ClientDelegate(Delegate):
self.client = client
def connection_start(self, ch, msg):
+
+ if self.client.mechanism is None:
+ if self.client.response is not None:
+ # Supports users passing the response argument alon
+ self.client.mechanism = "AMQPLAIN"
+ else:
+ supportedMechs = msg.frame.args[3].split()
+
+ self.client.sasl = get_sasl_mechanism(supportedMechs, self.client.username, self.client.password, sasl_options=self.client.sasl_options)
+
+ if self.client.sasl == None:
+ raise SaslException("sasl negotiation failed: no mechanism agreed. Server supports: %s " % supportedMechs)
+
+ self.client.mechanism = self.client.sasl.mechanismName()
+
+ if self.client.response is None:
+ self.client.response = self.client.sasl.initialResponse()
+
msg.start_ok(mechanism=self.client.mechanism,
- response=self.client.response,
+ response=self.client.response or "",
locale=self.client.locale,
client_properties=self.client.client_properties)
+ def connection_secure(self, ch, msg):
+ msg.secure_ok(response=self.client.sasl.response(msg.challenge))
+
def connection_tune(self, ch, msg):
if self.client.tune_params:
#todo: just override the params, i.e. don't require them
diff --git a/python/qpid/saslmech/__init__.py b/python/qpid/saslmech/__init__.py
new file mode 100644
index 0000000000..ebcab03ca2
--- /dev/null
+++ b/python/qpid/saslmech/__init__.py
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Pure python implementations of SASL mechanisms"""
+
+
diff --git a/python/qpid/saslmech/amqplain.py b/python/qpid/saslmech/amqplain.py
new file mode 100644
index 0000000000..d31997eefd
--- /dev/null
+++ b/python/qpid/saslmech/amqplain.py
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from sasl import Sasl, SaslException
+
+class AMQPLAIN(Sasl):
+
+ def initialResponse(self):
+ if (self.user is None or self.password is None):
+ raise SaslException("User and password must be specified")
+
+ return {"LOGIN": self.user, "PASSWORD": self.password}
+
+ def priority(self):
+ return 0 \ No newline at end of file
diff --git a/python/qpid/saslmech/anonymous.py b/python/qpid/saslmech/anonymous.py
new file mode 100644
index 0000000000..ea3e51dfba
--- /dev/null
+++ b/python/qpid/saslmech/anonymous.py
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from sasl import Sasl
+
+class ANONYMOUS(Sasl): pass
+
diff --git a/python/qpid/saslmech/cram_md5.py b/python/qpid/saslmech/cram_md5.py
new file mode 100644
index 0000000000..c2eb8078a0
--- /dev/null
+++ b/python/qpid/saslmech/cram_md5.py
@@ -0,0 +1,30 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from sasl import Sasl, SaslException
+from hmac import HMAC
+
+class CRAM_MD5(Sasl):
+
+ def response(self, challenge):
+ if (self.user is None or self.password is None):
+ raise SaslException("User and password must be specified")
+
+ digest = HMAC( self.password, challenge).hexdigest()
+ return "%s %s" % (self.user, digest)
diff --git a/python/qpid/saslmech/cram_md5_hex.py b/python/qpid/saslmech/cram_md5_hex.py
new file mode 100644
index 0000000000..39342060da
--- /dev/null
+++ b/python/qpid/saslmech/cram_md5_hex.py
@@ -0,0 +1,33 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from sasl import Sasl
+from hmac import HMAC
+from hashlib import md5
+
+class CRAM_MD5_HEX(Sasl):
+
+ def response(self, challenge):
+ if (self.user is None or self.password is None):
+ raise SaslException("User and password must be specified")
+
+ m = md5()
+ m.update(self.password)
+ digest = HMAC( m.hexdigest(), challenge).hexdigest()
+ return "%s %s" % (self.user, digest)
diff --git a/python/qpid/saslmech/external.py b/python/qpid/saslmech/external.py
new file mode 100644
index 0000000000..7b701f539d
--- /dev/null
+++ b/python/qpid/saslmech/external.py
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from sasl import Sasl
+
+
+class EXTERNAL(Sasl): pass \ No newline at end of file
diff --git a/python/qpid/saslmech/finder.py b/python/qpid/saslmech/finder.py
new file mode 100644
index 0000000000..15a8721f81
--- /dev/null
+++ b/python/qpid/saslmech/finder.py
@@ -0,0 +1,63 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+from logging import getLogger
+
+log = getLogger("qpid.saslmech")
+
+def get_sasl_mechanism(mechanismNames, username, password, namespace="qpid.saslmech", sasl_options=None):
+ """Given a list of SASL mechanism names, dynamically loads a SASL implementation
+ from namespace qpid.sasl.mech respecting a mechanism priority"""
+
+ log.debug("Supported mechanism : %s", mechanismNames)
+
+ instances = []
+ for mechanismName in mechanismNames:
+ convertedName = mechanismName.replace("-","_")
+ canonicalName = "%s.%s.%s" % (namespace, convertedName.lower(), convertedName)
+ try:
+ log.debug("Checking for SASL implementation %s for mechanism %s", canonicalName, mechanismName)
+ clazz = _get_class(canonicalName)
+ log.debug("Found SASL implementation")
+ instance = clazz(username, password, mechanismName, sasl_options)
+ instances.append(instance)
+ except (ImportError, AttributeError), e:
+ # Unknown mechanism - this is normal if the server supports m
+ log.debug("Could not load implementation for %s", canonicalName)
+ pass
+
+ if instances:
+ instances.sort(key=lambda x : x.priority(), reverse=True)
+ sasl = instances[0]
+ log.debug("Selected SASL mechanism %s", sasl.mechanismName())
+ return sasl
+ else:
+ return None
+
+def _get_class( kls ):
+ parts = kls.split('.')
+ module = ".".join(parts[:-1])
+ m = __import__( module )
+ for comp in parts[1:]:
+ m = getattr(m, comp)
+ return m
+
+
+
diff --git a/python/qpid/saslmech/plain.py b/python/qpid/saslmech/plain.py
new file mode 100644
index 0000000000..aa883e5460
--- /dev/null
+++ b/python/qpid/saslmech/plain.py
@@ -0,0 +1,31 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from sasl import Sasl, SaslException
+
+class PLAIN(Sasl):
+
+ def initialResponse(self):
+ if (self.user is None or self.password is None):
+ raise SaslException("User and password must be specified")
+
+ return "\x00" + self.user + "\x00" + self.password
+
+ def priority(self):
+ return 0 \ No newline at end of file
diff --git a/python/qpid/saslmech/sasl.py b/python/qpid/saslmech/sasl.py
new file mode 100644
index 0000000000..f6b7f04fb0
--- /dev/null
+++ b/python/qpid/saslmech/sasl.py
@@ -0,0 +1,41 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+class SaslException(Exception): pass
+
+class Sasl:
+
+ def __init__(self, user, password, name, sasl_options = None):
+ self.user = user
+ self.password = password
+ self.name = name
+ self.sasl_options = sasl_options
+
+ def initialResponse(self):
+ return
+
+ def response(self, challenge):
+ return
+
+ def priority(self):
+ """Priority of the mechanism. Mechanism with a higher value will be chosen in preference to those with a lower priority"""
+ return 1
+
+ def mechanismName(self):
+ return self.name \ No newline at end of file
diff --git a/python/qpid/saslmech/scram.py b/python/qpid/saslmech/scram.py
new file mode 100644
index 0000000000..d12f8c8fa5
--- /dev/null
+++ b/python/qpid/saslmech/scram.py
@@ -0,0 +1,94 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from hmac import HMAC
+from binascii import b2a_hex
+from sasl import Sasl
+import os
+import base64
+
+class SCRAM_base(Sasl):
+
+ def __init__(self, algorithm, user, password, name, sasl_options=None):
+ Sasl.__init__(self, user, password, name, sasl_options)
+ self.algorithm = algorithm
+ self.client_nonce = b2a_hex(os.urandom(16))
+ self.server_signature = None
+
+ def initialResponse(self):
+ if (self.user is None or self.password is None):
+ raise SaslException("User and password must be specified")
+
+ name = self.user.replace("=","=3D").replace(",","=2C")
+ self.client_first_message = "n=" + name + ",r=" + self.client_nonce
+ return "n,," + self.client_first_message
+
+
+ def response(self, challenge):
+ if(self.server_signature):
+ self.evaluateOutcome(challenge)
+ return ""
+ else:
+ serverChallenge, salt, iterations = challenge.split(",")
+ self.server_nonce = serverChallenge[2:]
+ if self.server_nonce.find(self.client_nonce) != 0:
+ raise SaslException("Server nonce does not start with client nonce")
+ self.salt = base64.b64decode(salt[2:])
+
+ iterations = int(iterations[2:])
+
+ hmac = HMAC(key=self.password.replace("=","=3D").replace(",","=2C"),digestmod=self.algorithm)
+
+ hmac.update(self.salt)
+ hmac.update("\x00\x00\x00\x01")
+
+ saltedPassword = hmac.digest()
+ previous = saltedPassword
+
+ for i in range(1,iterations):
+ hmac = HMAC(key=self.password.replace("=","=3D").replace(",","=2C"),digestmod=self.algorithm)
+ hmac.update(previous)
+ previous = hmac.digest()
+ saltedPassword = ''.join(chr(ord(a) ^ ord(b)) for a,b in zip(saltedPassword,previous))
+
+ clientFinalMessageWithoutProof = "c=" + base64.b64encode("n,,") + ",r=" + self.server_nonce
+ authMessage = self.client_first_message + "," + challenge + "," + clientFinalMessageWithoutProof
+
+ clientKey = HMAC(key=saltedPassword,msg="Client Key",digestmod=self.algorithm).digest()
+ hashFunc = self.algorithm()
+ hashFunc.update(clientKey)
+ storedKey = hashFunc.digest()
+
+ clientSignature = HMAC(key=storedKey, msg=authMessage, digestmod=self.algorithm).digest()
+
+ clientProof = ''.join(chr(ord(a) ^ ord(b)) for a,b in zip(clientKey,clientSignature))
+
+ serverKey = HMAC(key=saltedPassword,msg="Server Key",digestmod=self.algorithm).digest()
+
+ self.server_signature = HMAC(key=serverKey,msg=authMessage,digestmod=self.algorithm).digest()
+ return clientFinalMessageWithoutProof + ",p=" + base64.b64encode(clientProof)
+
+
+ def evaluateOutcome(self, challenge):
+ serverVerification = challenge.split(",")[0]
+
+ serverSignature = base64.b64decode(serverVerification[2:])
+ if serverSignature != self.server_signature:
+ raise SaslException("Server verification failed")
+ return
diff --git a/python/qpid/saslmech/scram_sha_1.py b/python/qpid/saslmech/scram_sha_1.py
new file mode 100644
index 0000000000..83f58f8e76
--- /dev/null
+++ b/python/qpid/saslmech/scram_sha_1.py
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from scram import SCRAM_base
+from hashlib import sha1
+
+class SCRAM_SHA_1(SCRAM_base):
+
+ def __init__(self, user, password, name, sasl_options=None):
+ SCRAM_base.__init__(self, sha1, user, password, name, sasl_options)
+
diff --git a/python/qpid/saslmech/scram_sha_256.py b/python/qpid/saslmech/scram_sha_256.py
new file mode 100644
index 0000000000..cc894895c0
--- /dev/null
+++ b/python/qpid/saslmech/scram_sha_256.py
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from scram import SCRAM_base
+import hashlib
+
+class SCRAM_SHA_256(SCRAM_base):
+ def __init__(self, user, password, name, sasl_options=None):
+ SCRAM_base.__init__(self,hashlib.sha256,user,password, name, sasl_options) \ No newline at end of file
diff --git a/python/qpid/testlib.py b/python/qpid/testlib.py
index d2617c5d18..0103956ec4 100644
--- a/python/qpid/testlib.py
+++ b/python/qpid/testlib.py
@@ -84,10 +84,7 @@ class TestBase(unittest.TestCase):
password = password or self.config.broker.password or "guest"
client = qpid.client.Client(host, port)
try:
- if client.spec.major == 8 and client.spec.minor == 0:
- client.start({"LOGIN": user, "PASSWORD": password}, tune_params=tune_params, client_properties=client_properties)
- else:
- client.start("\x00" + user + "\x00" + password, mechanism="PLAIN", tune_params=tune_params, client_properties=client_properties)
+ client.start(username = user, password=password, tune_params=tune_params, client_properties=client_properties)
except qpid.client.Closed, e:
if isinstance(e.args[0], VersionError):
raise Skipped(e.args[0])
diff --git a/python/qpid/tests/__init__.py b/python/qpid/tests/__init__.py
index dc9988515e..85ab013b5a 100644
--- a/python/qpid/tests/__init__.py
+++ b/python/qpid/tests/__init__.py
@@ -38,6 +38,7 @@ import qpid.tests.connection
import qpid.tests.spec010
import qpid.tests.codec010
import qpid.tests.util
+import qpid.tests.saslmech.finder
class TestTestsXXX(Test):
diff --git a/python/qpid/tests/saslmech/__init__.py b/python/qpid/tests/saslmech/__init__.py
new file mode 100644
index 0000000000..d8a500d9d8
--- /dev/null
+++ b/python/qpid/tests/saslmech/__init__.py
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
diff --git a/python/qpid/tests/saslmech/finder.py b/python/qpid/tests/saslmech/finder.py
new file mode 100644
index 0000000000..d9092654bc
--- /dev/null
+++ b/python/qpid/tests/saslmech/finder.py
@@ -0,0 +1,62 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from unittest import TestCase
+from qpid.saslmech.finder import get_sasl_mechanism
+from my_sasl import MY_SASL
+from my_sasl2 import MY_SASL2
+
+class SaslFinderTests (TestCase):
+ """Tests the ability to chose the a sasl mechanism from those available to be loaded"""
+
+ def test_known_mechansim(self):
+
+ supportedMechs = ["MY-SASL"]
+
+ mech = get_sasl_mechanism(supportedMechs, "myuser", "mypass", namespace="qpid.tests.saslmech")
+
+ self.assertTrue(isinstance(mech, MY_SASL), "Mechanism %s is of unexpected type" % mech)
+ self.assertEquals("MY-SASL", mech.mechanismName())
+ self.assertTrue(mech.sasl_options is None)
+
+ def test_unknown_mechansim(self):
+
+ supportedMechs = ["not_a_mech"]
+
+ mech = get_sasl_mechanism(supportedMechs, "myuser", "mypass", namespace="qpid.tests.saslmech")
+
+ self.assertTrue(mech == None, "Mechanism instance should be none")
+
+ def test_sasl_mechanism_with_higher_priority_prefered(self):
+
+ supportedMechs = ["MY-SASL", "MY-SASL2"]
+
+ mech = get_sasl_mechanism(supportedMechs, "myuser", "mypass", namespace="qpid.tests.saslmech")
+
+ self.assertTrue(isinstance(mech, MY_SASL), "Mechanism %s is of unexpected type" % mech)
+
+ def test_sasl_mechansim_options(self):
+
+ supportedMechs = ["MY-SASL"]
+
+ sasl_options = {'hello': 'world'}
+ mech = get_sasl_mechanism(supportedMechs, "myuser", "mypass", namespace="qpid.tests.saslmech", sasl_options=sasl_options)
+
+ self.assertTrue(isinstance(mech, MY_SASL), "Mechanism %s is of unexpected type" % mech)
+ self.assertEquals(sasl_options, mech.sasl_options)
diff --git a/python/qpid/tests/saslmech/my_sasl.py b/python/qpid/tests/saslmech/my_sasl.py
new file mode 100644
index 0000000000..c15fe4451c
--- /dev/null
+++ b/python/qpid/tests/saslmech/my_sasl.py
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from qpid.saslmech.sasl import Sasl
+
+class MY_SASL(Sasl): pass
diff --git a/python/qpid/tests/saslmech/my_sasl2.py b/python/qpid/tests/saslmech/my_sasl2.py
new file mode 100644
index 0000000000..f481a615e7
--- /dev/null
+++ b/python/qpid/tests/saslmech/my_sasl2.py
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from qpid.saslmech.sasl import Sasl
+
+class MY_SASL2(Sasl):
+
+ def priority(self):
+ return 0 \ No newline at end of file
diff --git a/tests/src/py/qpid_tests/broker_0_9/echo.py b/tests/src/py/qpid_tests/broker_0_9/echo.py
index 13305cbbf6..a883568e35 100644
--- a/tests/src/py/qpid_tests/broker_0_9/echo.py
+++ b/tests/src/py/qpid_tests/broker_0_9/echo.py
@@ -90,7 +90,7 @@ class EchoTests(TestBase):
# the content in order to send it to us.
consuming_client = qpid.client.Client(self.config.broker.host, self.config.broker.port)
tune_params = { "channel_max" : 256, "frame_max" : 4096 }
- consuming_client.start("\x00" + self.config.broker.user + "\x00" + self.config.broker.password, mechanism="PLAIN", tune_params = tune_params)
+ consuming_client.start(username = self.config.broker.user, password = self.config.broker.password, tune_params = tune_params)
consuming_channel = consuming_client.channel(1)
consuming_channel.channel_open()