From 2bcc371558ce0659f53b86046cdf3d5de3b20910 Mon Sep 17 00:00:00 2001 From: Robert Greig Date: Mon, 29 Jan 2007 10:46:27 +0000 Subject: (Patch supplied by Tomas Restrepo) QPID-291-2.diff applied. Adds SASL capability to the .Net client. git-svn-id: https://svn.apache.org/repos/asf/incubator/qpid/trunk/qpid@501001 13f79535-47bb-0310-9956-ffa450edef68 --- .../Client/AMQAuthenticationException.cs | 39 +++++++ dotnet/Qpid.Client/Client/AMQConnection.cs | 2 +- .../AuthenticationConfigurationSectionHandler.cs | 64 ++++++++++++ .../Client/Handler/ChannelCloseMethodHandler.cs | 5 +- .../Client/Handler/ConnectionCloseMethodHandler.cs | 25 ++++- .../Handler/ConnectionSecureMethodHandler.cs | 27 ++++- .../Client/Handler/ConnectionStartMethodHandler.cs | 81 +++++++++++---- .../Client/Protocol/AMQProtocolSession.cs | 8 ++ .../Client/Security/CallbackHandlerRegistry.cs | 114 +++++++++++++++++++++ .../Client/Security/IAMQCallbackHandler.cs | 34 ++++++ .../Security/UsernamePasswordCallbackHandler.cs | 56 ++++++++++ 11 files changed, 423 insertions(+), 32 deletions(-) create mode 100644 dotnet/Qpid.Client/Client/AMQAuthenticationException.cs create mode 100644 dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs create mode 100644 dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs create mode 100644 dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs create mode 100644 dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs (limited to 'dotnet/Qpid.Client/Client') diff --git a/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs b/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs new file mode 100644 index 0000000000..68cacad1ef --- /dev/null +++ b/dotnet/Qpid.Client/Client/AMQAuthenticationException.cs @@ -0,0 +1,39 @@ +/* + * + * 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. + * + */ +using System; +using System.Runtime.Serialization; + +namespace Qpid.Client +{ + [Serializable] + public class AMQAuthenticationException : AMQException + { + public AMQAuthenticationException(int error, String message) + : base(error, message) + { + } + + protected AMQAuthenticationException(SerializationInfo info, StreamingContext ctxt) + : base(info, ctxt) + { + } + } +} diff --git a/dotnet/Qpid.Client/Client/AMQConnection.cs b/dotnet/Qpid.Client/Client/AMQConnection.cs index 1da46f19fd..3192b0018d 100644 --- a/dotnet/Qpid.Client/Client/AMQConnection.cs +++ b/dotnet/Qpid.Client/Client/AMQConnection.cs @@ -730,7 +730,7 @@ namespace Qpid.Client catch (AMQException e) { _lastAMQException = e; - throw e; + throw; // rethrow } } diff --git a/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs b/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs new file mode 100644 index 0000000000..54ee2c6660 --- /dev/null +++ b/dotnet/Qpid.Client/Client/Configuration/AuthenticationConfigurationSectionHandler.cs @@ -0,0 +1,64 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Configuration; +using System.Text; + +using Qpid.Client.Security; +using Qpid.Sasl.Mechanisms; + +namespace Qpid.Client.Configuration +{ + public class AuthenticationConfigurationSectionHandler + : IConfigurationSectionHandler + { + + public object Create(object parent, object configContext, System.Xml.XmlNode section) + { + NameValueSectionHandler handler = new NameValueSectionHandler(); + IDictionary schemes = new Hashtable(); + + NameValueCollection options = (NameValueCollection) + handler.Create(parent, configContext, section); + + if ( options != null ) + { + foreach ( string key in options.Keys ) + { + Type type = Type.GetType(options[key]); + if ( type == null ) + throw new ConfigurationException(string.Format("Type '{0}' not found", key)); + if ( !typeof(IAMQCallbackHandler).IsAssignableFrom(type) ) + throw new ConfigurationException(string.Format("Type '{0}' does not implement IAMQCallbackHandler", key)); + + schemes[key] = type; + } + } + + return schemes; + } + + } // class AuthenticationConfigurationSectionHandler + +} // namespace Qpid.Client.Configuration diff --git a/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs b/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs index 1031f804a6..0ce8a393c9 100644 --- a/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs +++ b/dotnet/Qpid.Client/Client/Handler/ChannelCloseMethodHandler.cs @@ -21,6 +21,7 @@ using log4net; using Qpid.Client.Protocol; using Qpid.Client.State; +using Qpid.Protocol; using Qpid.Framing; namespace Qpid.Client.Handler @@ -43,9 +44,8 @@ namespace Qpid.Client.Handler AMQFrame frame = ChannelCloseOkBody.CreateAMQFrame(evt.ChannelId); evt.ProtocolSession.WriteFrame(frame); - //if (errorCode != AMQConstant.REPLY_SUCCESS.getCode()) // HACK - if (errorCode != 200) + if ( errorCode != AMQConstant.REPLY_SUCCESS.Code ) { _logger.Debug("Channel close received with errorCode " + errorCode + ", throwing exception"); evt.ProtocolSession.AMQConnection.ExceptionReceived(new AMQChannelClosedException(errorCode, "Error: " + reason)); @@ -55,3 +55,4 @@ namespace Qpid.Client.Handler } } + diff --git a/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs b/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs index c3acc0b098..dea5316d25 100644 --- a/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs +++ b/dotnet/Qpid.Client/Client/Handler/ConnectionCloseMethodHandler.cs @@ -19,10 +19,12 @@ * */ using System; +using System.Threading; using log4net; using Qpid.Client.Protocol; using Qpid.Client.State; using Qpid.Framing; +using Qpid.Protocol; namespace Qpid.Client.Handler { @@ -38,16 +40,29 @@ namespace Qpid.Client.Handler int errorCode = method.ReplyCode; String reason = method.ReplyText; + // send CloseOK evt.ProtocolSession.WriteFrame(ConnectionCloseOkBody.CreateAMQFrame(evt.ChannelId)); - stateManager.ChangeState(AMQState.CONNECTION_CLOSED); - if (errorCode != 200) + + if ( errorCode != AMQConstant.REPLY_SUCCESS.Code ) { - _logger.Debug("Connection close received with error code " + errorCode); - throw new AMQConnectionClosedException(errorCode, "Error: " + reason); - } + if ( errorCode == AMQConstant.NOT_ALLOWED.Code ) + { + _logger.Info("Authentication Error: " + Thread.CurrentThread.Name); + evt.ProtocolSession.CloseProtocolSession(); + + //todo this is a bit of a fudge (could be conssidered such as each new connection needs a new state manager or at least a fresh state. + stateManager.ChangeState(AMQState.CONNECTION_NOT_STARTED); + throw new AMQAuthenticationException(errorCode, reason); + } else + { + _logger.Info("Connection close received with error code " + errorCode); + throw new AMQConnectionClosedException(errorCode, "Error: " + reason); + } + } // this actually closes the connection in the case where it is not an error. evt.ProtocolSession.CloseProtocolSession(); + stateManager.ChangeState(AMQState.CONNECTION_CLOSED); } } } diff --git a/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs b/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs index 7c0fbd8f40..fe123e6745 100644 --- a/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs +++ b/dotnet/Qpid.Client/Client/Handler/ConnectionSecureMethodHandler.cs @@ -21,6 +21,7 @@ using Qpid.Client.Protocol; using Qpid.Client.State; using Qpid.Framing; +using Qpid.Sasl; namespace Qpid.Client.Handler { @@ -28,9 +29,31 @@ namespace Qpid.Client.Handler { public void MethodReceived(AMQStateManager stateManager, AMQMethodEvent evt) { - AMQFrame response = ConnectionSecureOkBody.CreateAMQFrame(evt.ChannelId, null); - evt.ProtocolSession.WriteFrame(response); + ISaslClient saslClient = evt.ProtocolSession.SaslClient; + if ( saslClient == null ) + { + throw new AMQException("No SASL client set up - cannot proceed with authentication"); + } + + + ConnectionSecureBody body = (ConnectionSecureBody)evt.Method; + + try + { + // Evaluate server challenge + byte[] response = saslClient.EvaluateChallenge(body.Challenge); + // AMQP version change: Hardwire the version to 0-8 (major=8, minor=0) + // TODO: Connect this to the session version obtained from ProtocolInitiation for this session. + // Be aware of possible changes to parameter order as versions change. + AMQFrame responseFrame = ConnectionSecureOkBody.CreateAMQFrame( + evt.ChannelId, response); + evt.ProtocolSession.WriteFrame(responseFrame); + } catch ( SaslException e ) + { + throw new AMQException("Error processing SASL challenge: " + e, e); + } } } } + diff --git a/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs b/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs index e88ff3ddbd..1815bea152 100644 --- a/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs +++ b/dotnet/Qpid.Client/Client/Handler/ConnectionStartMethodHandler.cs @@ -19,11 +19,15 @@ * */ using System; +using System.Collections; using System.Text; using log4net; using Qpid.Client.Protocol; +using Qpid.Client.Security; using Qpid.Client.State; using Qpid.Framing; +using Qpid.Sasl; + namespace Qpid.Client.Handler { @@ -35,36 +39,22 @@ namespace Qpid.Client.Handler { ConnectionStartBody body = (ConnectionStartBody) evt.Method; AMQProtocolSession ps = evt.ProtocolSession; - string username = ps.Username; - string password = ps.Password; try { - if (body.Mechanisms == null) + if ( body.Mechanisms == null ) { throw new AMQException("mechanism not specified in ConnectionStart method frame"); } - string allMechanisms = Encoding.ASCII.GetString(body.Mechanisms); - string[] mechanisms = allMechanisms.Split(' '); - string selectedMechanism = null; - foreach (string mechanism in mechanisms) - { - if (mechanism.Equals("PLAIN")) - { - selectedMechanism = mechanism; - break; - } - } - - if (selectedMechanism == null) + string mechanisms = Encoding.UTF8.GetString(body.Mechanisms); + string selectedMechanism = ChooseMechanism(mechanisms); + if ( selectedMechanism == null ) { throw new AMQException("No supported security mechanism found, passed: " + mechanisms); } + + byte[] saslResponse = DoAuthentication(selectedMechanism, ps); - // we always write out a null authzid which we don't currently use - byte[] plainData = new byte[1 + ps.Username.Length + 1 + ps.Password.Length]; - Encoding.UTF8.GetBytes(username, 0, username.Length, plainData, 1); - Encoding.UTF8.GetBytes(password, 0, password.Length, plainData, username.Length + 2); if (body.Locales == null) { throw new AMQException("Locales is not defined in Connection Start method"); @@ -86,8 +76,9 @@ namespace Qpid.Client.Handler clientProperties["product"] = "Qpid.NET"; clientProperties["version"] = "1.0"; clientProperties["platform"] = GetFullSystemInfo(); - AMQFrame frame = ConnectionStartOkBody.CreateAMQFrame(evt.ChannelId, clientProperties, selectedMechanism, - plainData, selectedLocale); + AMQFrame frame = ConnectionStartOkBody.CreateAMQFrame( + evt.ChannelId, clientProperties, selectedMechanism, + saslResponse, selectedLocale); ps.WriteFrame(frame); } catch (Exception e) @@ -109,5 +100,51 @@ namespace Qpid.Client.Handler // TODO: add in details here return ".NET 1.1 Client"; } + + private string ChooseMechanism(string mechanisms) + { + foreach ( string mech in mechanisms.Split(' ') ) + { + if ( CallbackHandlerRegistry.Instance.IsSupportedMechanism(mech) ) + { + return mech; + } + } + return null; + } + + private byte[] DoAuthentication(string selectedMechanism, AMQProtocolSession ps) + { + ISaslClient saslClient = Sasl.Sasl.CreateClient( + new string[] { selectedMechanism }, null, "AMQP", "localhost", + new Hashtable(), CreateCallbackHandler(selectedMechanism, ps) + ); + if ( saslClient == null ) + { + throw new AMQException("Client SASL configuration error: no SaslClient could be created for mechanism " + + selectedMechanism); + } + ps.SaslClient = saslClient; + try + { + return saslClient.HasInitialResponse ? + saslClient.EvaluateChallenge(new byte[0]) : null; + } catch ( Exception ex ) + { + ps.SaslClient = null; + throw new AMQException("Unable to create SASL client", ex); + } + } + + private IAMQCallbackHandler CreateCallbackHandler(string mechanism, AMQProtocolSession session) + { + Type type = CallbackHandlerRegistry.Instance.GetCallbackHandler(mechanism); + IAMQCallbackHandler handler = + (IAMQCallbackHandler)Activator.CreateInstance(type); + if ( handler == null ) + throw new AMQException("Unable to create callback handler: " + mechanism); + handler.Initialize(session); + return handler; + } } } diff --git a/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs b/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs index 15696f38c5..42169f31b3 100644 --- a/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs +++ b/dotnet/Qpid.Client/Client/Protocol/AMQProtocolSession.cs @@ -24,6 +24,7 @@ using log4net; using Qpid.Client.Message; using Qpid.Client.Transport; using Qpid.Framing; +using Qpid.Sasl; namespace Qpid.Client.Protocol { @@ -104,6 +105,13 @@ namespace Qpid.Client.Protocol con.MaximumFrameSize = value.FrameMax; } } + + private ISaslClient _saslClient; + public ISaslClient SaslClient + { + get { return _saslClient; } + set { _saslClient = value; } + } /// /// Callback invoked from the BasicDeliverMethodHandler when a message has been received. diff --git a/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs b/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs new file mode 100644 index 0000000000..78f13c9f42 --- /dev/null +++ b/dotnet/Qpid.Client/Client/Security/CallbackHandlerRegistry.cs @@ -0,0 +1,114 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Configuration; +using System.Text; +using Qpid.Sasl; +using Qpid.Sasl.Mechanisms; + +using Qpid.Client.Configuration; + +namespace Qpid.Client.Security +{ + + /// + /// Helper class to map SASL mechanisms to our + /// internal ISaslCallbackHandler implementations. + /// + /// + /// The set of configured callback handlers and their order + /// controls the selection of the SASL mechanism used for authentication. + /// + /// You can either replace the default handler for CRAM-MD5 and PLAIN + /// authentication (the two default options) using the application + /// configuration file. Configuration is done by especifying the SASL + /// mechanism name (e.g PLAIN) and the type implementing the callback handler + /// used to provide any data required by the mechanism like username and password. + /// + /// + /// Callback handler types should implement the IAMQCallbackHandler interface. + /// + /// + /// New callbacks or authentication mechanisms can be configured like this: + /// + /// + /// + /// + ///
+ /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + public sealed class CallbackHandlerRegistry + { + private static CallbackHandlerRegistry _instance = + new CallbackHandlerRegistry(); + private IDictionary _mechanism2HandlerMap; + private string[] _mechanisms; + + public static CallbackHandlerRegistry Instance + { + get { return _instance; } + } + + public string[] Mechanisms + { + get { return _mechanisms; } + } + + private CallbackHandlerRegistry() + { + _mechanism2HandlerMap = (IDictionary) + ConfigurationSettings.GetConfig("qpid.client/authentication"); + + // configure default options if not available + if ( _mechanism2HandlerMap == null ) + _mechanism2HandlerMap = new Hashtable(); + + if ( !_mechanism2HandlerMap.Contains(CramMD5SaslClient.Mechanism) ) + _mechanism2HandlerMap.Add(CramMD5SaslClient.Mechanism, typeof(UsernamePasswordCallbackHandler)); + if ( !_mechanism2HandlerMap.Contains(PlainSaslClient.Mechanism) ) + _mechanism2HandlerMap.Add(PlainSaslClient.Mechanism, typeof(UsernamePasswordCallbackHandler)); + + _mechanisms = new string[_mechanism2HandlerMap.Keys.Count]; + _mechanism2HandlerMap.Keys.CopyTo(_mechanisms, 0); + } + + public bool IsSupportedMechanism(string mechanism) + { + return _mechanism2HandlerMap.Contains(mechanism); + } + + public Type GetCallbackHandler(string mechanism) + { + return (Type)_mechanism2HandlerMap[mechanism]; + } + } +} diff --git a/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs b/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs new file mode 100644 index 0000000000..6802b90cee --- /dev/null +++ b/dotnet/Qpid.Client/Client/Security/IAMQCallbackHandler.cs @@ -0,0 +1,34 @@ +/* + * + * 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. + * + */ +using System; +using System.Text; +using Qpid.Client.Protocol; +using Qpid.Sasl; + +namespace Qpid.Client.Security +{ + public interface IAMQCallbackHandler : ISaslCallbackHandler + { + void Initialize(AMQProtocolSession session); + } + +} + diff --git a/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs b/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs new file mode 100644 index 0000000000..a14139496c --- /dev/null +++ b/dotnet/Qpid.Client/Client/Security/UsernamePasswordCallbackHandler.cs @@ -0,0 +1,56 @@ +/* + * + * 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. + * + */ + +using System; +using System.Collections; +using System.Text; +using Qpid.Client.Protocol; +using Qpid.Sasl; + +namespace Qpid.Client.Security +{ + internal class UsernamePasswordCallbackHandler : IAMQCallbackHandler + { + private AMQProtocolSession _session; + + public void Initialize(AMQProtocolSession session) + { + if ( session == null ) + throw new ArgumentNullException("session"); + + _session = session; + } + + public void Handle(ISaslCallback[] callbacks) + { + foreach ( ISaslCallback cb in callbacks ) + { + if ( cb is NameCallback ) + { + ((NameCallback)cb).Text = _session.Username; + } else if ( cb is PasswordCallback ) + { + ((PasswordCallback)cb).Text = _session.Password; + } + } + } + } +} -- cgit v1.2.1