using System; using System.IO; using System.Web; using System.Data; using System.Text; using System.Xml; using System.Data.SqlClient; using System.Xml.Serialization; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Web.Services.Protocols; using System.Web.Security; using UDDI; using UDDI.Diagnostics; using UDDI.API.Authentication; namespace UDDI.API { public class UDDIExtension : SoapExtension { Data data; DateTime begin; private class Data { public bool log = true; public bool https = false; public bool validate = true; public bool performance = true; public bool authenticate = false; public bool transaction = false; public bool certificate = false; public string messageType = ""; public Data( bool log, bool validate, bool performance, bool authenticate, bool transaction, bool https, bool certificate, string messageType ) { this.log = log; this.https = https; this.validate = validate; this.performance = performance; this.authenticate = authenticate; this.transaction = transaction; this.certificate = certificate; this.messageType = messageType; } } static UDDIExtension() { Context.ContextType = ContextType.SOAP; } private void CheckForHttps( SoapMessage message ) { Debug.Enter(); if( 1 == Config.GetInt( "Security.HTTPS", 1 ) ) { Debug.Write( SeverityType.Info, CategoryType.Soap, "URL: " + message.Url ); Debug.Verify( message.Url.ToLower().StartsWith( "https" ), "UDDI_ERROR_FATALERROR_HTTPSREQUIREDFORPUBLISH" ); } else { Debug.Write( SeverityType.Warning, CategoryType.Soap, "HTTPS check is turned off. Content may be published without SSL. To turn this check on remove or modify the Security.HTTPS configuration setting" ); } Debug.Leave(); } private void CheckCertificate( SoapMessage message ) { HttpClientCertificate httpCert = HttpContext.Current.Request.ClientCertificate; X509Certificate requestCert = new X509Certificate( httpCert.Certificate ); Debug.Verify( !Utility.StringEmpty( httpCert.Issuer ), "UDDI_ERROR_FATALERROR_CLIENTCERTREQUIRED" ); Debug.Verify( !Utility.StringEmpty( httpCert.Subject ), "UDDI_ERROR_FATALERROR_CLIENTCERTREQUIRED" ); SqlStoredProcedureAccessor sp = new SqlStoredProcedureAccessor( "net_operatorCert_get" ); sp.Parameters.Add( "@certSerialNo", SqlDbType.NVarChar, UDDI.Constants.Lengths.CertSerialNo ); sp.Parameters.SetString( "@certSerialNo", requestCert.GetSerialNumberString() ); SqlDataReaderAccessor reader = sp.ExecuteReader(); try { if( reader.Read() ) { Context.RemoteOperator = reader.GetGuidString( "operatorKey" ); byte[] operatorCertRaw = reader.GetBinary( "certificate" ); byte[] requestCertRaw = httpCert.Certificate; Debug.Verify( null != operatorCertRaw, "UDDI_ERROR_FATALERROR_CLIENTCERTNOTSTORED", ErrorType.E_fatalError, Context.RemoteOperator ); if( operatorCertRaw.Length != requestCertRaw.Length ) { throw new UDDIException( ErrorType.E_unknownUser, "UDDI_ERROR_UNKNOWNUSER_UNKOWNCERT" ); } for( int i = 0; i < operatorCertRaw.Length; i ++ ) { if( operatorCertRaw[ i ] != requestCertRaw[ i ] ) { throw new UDDIException( ErrorType.E_unknownUser, "UDDI_ERROR_UNKNOWNUSER_UNKOWNCERT" ); } } /* * TODO: Check to see if this works instead * X509Certificate operatorCert = new X509Certificate( operatorCertRaw ); X509Certificate requestCert = new X509Certificate( requestCertRaw ); if( !requestCert.Equals( operatorCert ) ) { throw new UDDIException( ErrorType.E_unknownUser, "Unknown certificate" ); } */ } else { throw new UDDIException( ErrorType.E_unknownUser, "UDDI_ERROR_UNKNOWNUSER_UNKOWNCERT" ); } } finally { reader.Close(); } } private void Validate( SoapMessage message ) { Debug.Enter(); StreamReader srdr = new StreamReader( message.Stream, System.Text.Encoding.UTF8 ); #if DEBUG Debug.Write( SeverityType.Verbose, CategoryType.None, srdr.ReadToEnd() ); message.Stream.Seek( 0,System.IO.SeekOrigin.Begin ); #endif // // Validate incoming XML, ValidateStream will rewind stream when finished // so I don't have to. // SchemaCollection.Validate( message.Stream ); Debug.Leave(); } private void PublishMethodBegin( SoapMessage message ) { Debug.Enter(); begin = DateTime.Now; Debug.Leave(); } private void PublishMethodEnd( SoapMessage message ) { Debug.Enter(); TimeSpan duration = DateTime.Now - begin; Debug.Write( SeverityType.Info, CategoryType.Soap, "Message took " + duration.TotalMilliseconds.ToString() + " ms" ); Performance.PublishMessageData( data.messageType, duration ); Debug.Leave(); } // // What follows is the logic for selection of the authentication algorithm // Enjoy boys and girls // // Bit 3 - Anonymous User // Bit 2 - UDDI Authentication Mode // Bit 1 - Windows Authentication Mode // Bit 0 - Ticket Present // | // | Authentication Module Used // 0000 X // 0001 X // 0010 Windows // 0011 Exception ( UDDI authentication turned off ) // 0100 UDDI ( will fail authentication due to invalid credentials ) // 0101 UDDI // 0110 Windows // 0111 UDDI // 1000 X // 1001 X // 1010 Exception UDDI authentication turned off // 1011 Exception "" // 1100 UDDI ( will fail authentication due to invalid credentials ) // 1101 UDDI // 1110 UDDI ( will fail authentication due to invalid credentials ) // 1111 UDDI // // // Reduction Work // // A - Anonymous User // B - UDDI Authentication Mode // C - Windows Authentication Mode // D - Ticket Present // // Key // e - throw exception invalid configuration // x - invalid state // w - windows authentication // u - uddi authentication // // CD // AB 00 01 11 10 // 00 x x e w // 01 u u u w // 11 u u u u // 10 x x e e // // if( !A && C && !D ) // w - windows authentication // else if( B ) // u - uddi authentication // else // throw exception // private void Authenticate( SoapMessage message ) { Debug.Enter(); IAuthenticateable authenticate = (IAuthenticateable) message.GetInParameterValue(0); //WindowsIdentity identity = (WindowsIdentity)HttpContext.Current.User.Identity; IIdentity identity = HttpContext.Current.User.Identity; int mode = Config.GetInt( "Security.AuthenticationMode", (int) AuthenticationMode.Both ); if( mode == (int) AuthenticationMode.Passport ) { if( identity is PassportIdentity ) { string ticket = authenticate.AuthInfo.Trim(); // // Authentication the user using the attached passport ticket // PassportAuthenticator pa = new PassportAuthenticator(); pa.Authenticate( ticket, Config.GetInt( "Security.TimeOut", 60 ) ); } else { throw new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_PASSPORTBADCONFIG" ) ; } Debug.Write( SeverityType.Info, CategoryType.Soap, "Authenticated user: using Passport based authentication Identity is " + identity.Name ); } else if( !( (WindowsIdentity)identity ).IsAnonymous && ( ( mode & (int) AuthenticationMode.Windows ) != 0 ) && Utility.StringEmpty( authenticate.AuthInfo ) ) { /* 0X10 Case */ // // Authenticate the user using the currently impersonated credentials // WindowsAuthenticator wa = new WindowsAuthenticator(); wa.Authenticate( authenticate.AuthInfo, Config.GetInt( "Security.TimeOut", 60 ) ); Debug.Write( SeverityType.Info, CategoryType.Soap, "Authenticated user: using Windows based authentication Identity is " + identity.Name ); } else if( ( mode & (int) AuthenticationMode.Uddi ) != 0 ) { /* X1XX Case for leftovers */ // // If windows authentication is turned off or the Debug.Write( SeverityType.Info, CategoryType.Soap, "Anonymous user: using UDDI authentication" ); // // Authenticate the user using the authToken // UDDIAuthenticator ua = new UDDIAuthenticator(); ua.Authenticate( authenticate.AuthInfo, Config.GetInt( "Security.TimeOut", 60 ) ); } else { // // Throw exception for the rest // throw new UDDIException( UDDI.ErrorType.E_unsupported, "UDDI_ERROR_UNSUPPORTED_BADAUTHENTICATIONCONFIG" ); } // // Check to make sure the authenticated user has publisher credentials // Debug.Verify( Context.User.IsPublisher, "UDDI_ERROR_FATALERROR_USERNOPUBLISHERCRED", UDDI.ErrorType.E_fatalError, Context.User.ID ); // // The server can be configured for automatic registration of publishers with credentials // if( !Context.User.IsRegistered ) { if( 1 == Config.GetInt( "Security.AutoRegister", 0 ) ) { // // Mark the user as verified. // Context.User.TrackPassport = false; Context.User.Verified = true; Context.User.Register(); } else { throw new UDDIException( UDDI.ErrorType.E_unknownUser, "UDDI_ERROR_UNKNOWNUSER_NOTREGISTERED" ); } } Context.User.Login(); #if DEBUG Debug.Write( SeverityType.Info, CategoryType.Soap, "Windows Identity is " + WindowsIdentity.GetCurrent().Name ); Debug.Write( SeverityType.Info, CategoryType.Soap, "Thread Identity is " + System.Threading.Thread.CurrentPrincipal.Identity.Name ); Debug.Write( SeverityType.Info, CategoryType.Soap, "HttpContext Identity is " + identity.Name ); Debug.Write( SeverityType.Info, CategoryType.Soap, "IsAdministrator = " + Context.User.IsAdministrator ); Debug.Write( SeverityType.Info, CategoryType.Soap, "IsCoordinator = " + Context.User.IsCoordinator ); Debug.Write( SeverityType.Info, CategoryType.Soap, "IsPublisher = " + Context.User.IsPublisher ); Debug.Write( SeverityType.Info, CategoryType.Soap, "IsUser = " + Context.User.IsUser ); #endif Debug.Leave(); } public override object GetInitializer( Type t ) { return null; } public override object GetInitializer( LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute ) { UDDIExtensionAttribute attr = (UDDIExtensionAttribute) attribute; return new Data( attr.log, attr.validate, attr.performance, attr.authenticate, attr.transaction, attr.https, attr.certificate, attr.messageType ); } public override void Initialize( object initializer ) { data = (UDDIExtension.Data) initializer; } public override void ProcessMessage(SoapMessage message) { Debug.Enter(); #if DEBUG string info = "log: " + data.log.ToString() + "; https: " + data.https.ToString() + "; validate: " + data.validate.ToString() + "; performance: " + data.performance.ToString() + "; authenticate: " + data.authenticate.ToString() + "; transaction: " + data.transaction.ToString() + "; messageType: " + data.messageType; Debug.Write( SeverityType.Info, CategoryType.Soap, info ); #endif try { switch( message.Stage ) { // // First Event // case SoapMessageStage.BeforeDeserialize: // // Initialize our context. // Context.Current.Initialize(); Config.CheckForUpdate(); // // TODO: Since we are using DispositionReport.ThrowFinal() I don't think this is // needed anymore. // // // Check to make sure the authenticated user has user credentials // Debug.Verify( "1" != HttpContext.Current.Request.ServerVariables[ "Exception" ], "UDDI_ERROR_FATALERROR_VERSIONCHECKERROR", UDDI.ErrorType.E_fatalError ); Debug.Write( SeverityType.Info, CategoryType.Soap, "URL: " + message.Url ); Debug.Write( SeverityType.Info, CategoryType.Soap, "SOAPAction: " + HttpContext.Current.Request.Headers[ "SOAPAction" ] ); string contentType = HttpContext.Current.Request.ContentType.ToLower(); bool validEncoding = ( contentType.IndexOf( "charset=\"utf-8\"" ) >= 0 ) || ( contentType.IndexOf( "charset=utf-8" ) >= 0 ); Debug.Verify( validEncoding, "UDDI_ERROR_UNSUPPORTED_CONTENTTYPEHEADERMISSING", ErrorType.E_unsupported ); if( data.performance ) PublishMethodBegin( message ); if( data.https ) CheckForHttps( message ); // // Validation has been moved into the other SOAP extension // // if( data.validate ) // Validate( message ); break; // // Second Event // case SoapMessageStage.AfterDeserialize: ConnectionManager.Open( data.transaction, data.transaction ); if( data.certificate ) CheckCertificate( message ); if( data.authenticate ) Authenticate( message ); else if( 0 != ( Config.GetInt( "Security.AuthenticationMode", (int) AuthenticationMode.Both ) & (int) AuthenticationMode.AuthenticatedRead ) ) { // // Authenticated reads are turned on and this is a read request // Make sure the caller is authenticated using Windows and is at least a user // WindowsIdentity identity = (WindowsIdentity) HttpContext.Current.User.Identity; WindowsAuthenticator wa = new WindowsAuthenticator(); wa.Authenticate( "", 0 /* not used */ ); Debug.Write( SeverityType.Info, CategoryType.Soap, "Authenticated user: using Windows based authentication Identity is " + identity.Name ); // // Check to make sure the authenticated user has user credentials // Debug.Verify( Context.User.IsUser, "UDDI_ERROR_FATALERROR_NOUSERCREDS", UDDI.ErrorType.E_fatalError, Context.User.ID ); } break; // // Third Event // case SoapMessageStage.BeforeSerialize: break; // // Last Event // case SoapMessageStage.AfterSerialize: // // Cleanup the connection and commit the database activity // if( data.transaction && ( null != (object) ConnectionManager.GetConnection() ) && ( null != (object) ConnectionManager.GetTransaction() ) ) { if( null == (object) message.Exception ) { ConnectionManager.Commit(); } else { ConnectionManager.Abort(); } } ConnectionManager.Close(); try { if( data.performance ) PublishMethodEnd( message ); } catch { Debug.OperatorMessage( SeverityType.Warning, CategoryType.None, OperatorMessageType.UnableToPublishCounter, "An error occurred while trying to publish a performance counter, the system will continue" ); } break; default: throw new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_UNKNOWNEXTSTAGE" ); } } catch( Exception e ) { DispositionReport.Throw( e ); } Debug.Leave(); } public override Stream ChainStream( Stream stream ) { return base.ChainStream( stream ); } } [AttributeUsage(AttributeTargets.Method, AllowMultiple=false)] public class UDDIExtensionAttribute : SoapExtensionAttribute { private int priority; // // The default constructor should be configured for the inquire API set // public UDDIExtensionAttribute() : this( true, true, true, false, false, false, false, "" ){} public UDDIExtensionAttribute( bool log, bool validate, bool performance, bool authenticate, bool transaction, bool https, bool certificate, string messageType ) { this.log = log; this.https = https; this.validate = validate; this.performance = performance; this.authenticate = authenticate; this.transaction = transaction; this.certificate = certificate; this.messageType = messageType; } public override Type ExtensionType { get { return typeof(UDDIExtension); } } public override int Priority { get { return priority; } set { priority = value; } } public bool log; public bool https; public bool validate; public bool performance; public bool authenticate; public bool transaction; public bool certificate; public string messageType; } /// ******************************************************************** /// public class VersionSupportExtension /// -------------------------------------------------------------------- /// /// /// ******************************************************************** /// public class VersionSupportExtension : SoapExtension { Stream oldStream; Stream newStream; public override object GetInitializer( LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute ) { return null; } public override object GetInitializer( Type type ) { return null; } public override void Initialize( object initializer ) { } public override void ProcessMessage(SoapMessage message) { try { switch( message.Stage ) { case SoapMessageStage.BeforeDeserialize: // // Check to see if the server has been manually stopped. // if( 0 == Config.GetInt( "Run", 1 ) ) { DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_busy, "UDDI_ERROR_BUSY_SERVICENOTAVAILABLE" ) ); // // DispositionReport.ThrowFinal will close the HTTP stream so there is no point going on in this method // return; } try { // // Validate against the UDDI schemas // SchemaCollection.Validate( oldStream ); } catch( Exception e ) { DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_SCHEMAVALIDATIONFAILED", e.Message ) ); // // DispositionReport.ThrowFinal will close the HTTP stream so there is no point going on in this method // return; } // // Make sure we only have 1 UDDI request in the SOAP body. This method will also set the versionMajor // member. // CheckForSingleRequest( oldStream ); // // If this is a v1 message, we'll first map it to the v2 // namespace so that it can be processed by the new // library. // if( 1 == Context.ApiVersionMajor || 2 == Context.ApiVersionMajor) { TextReader reader = new StreamReader( oldStream ); TextWriter writer = new StreamWriter( newStream, new System.Text.UTF8Encoding( false ) ); string xml = reader.ReadToEnd(); if( 1 == Context.ApiVersionMajor ) { xml = xml.Replace( "=\"urn:uddi-org:api\"", "=\"urn:uddi-org:api_v2\"" ); xml = xml.Replace( "='urn:uddi-org:api'", "=\"urn:uddi-org:api_v2\"" ); } writer.Write( xml ); writer.Flush(); newStream.Position = 0; } break; case SoapMessageStage.AfterDeserialize: // // After the message is deserialized is the earliest place where we // have access to our SOAP headers. // CheckSOAPHeaders( message ); // // Now that the message has been deserialized, make // sure that the generic and xmlns attributes agree. // IMessage obj = message.GetInParameterValue( 0 ) as IMessage; if( null != obj ) { // // We only need to do this if the deserialized object supports IMessage // string expected = Context.ApiVersionMajor + ".0"; string actual = obj.Generic.Trim(); if( expected != actual ) throw new UDDIException( ErrorType.E_unrecognizedVersion, "UDDI_ERROR_UNKNOWNVERSION_GENERICNAMESPACEMISMATCH" ); } break; case SoapMessageStage.BeforeSerialize: break; case SoapMessageStage.AfterSerialize: // // There may have been exceptions thrown during serialization. // if( null != message.Exception && ( null == message.Exception.Detail || 0 == message.Exception.Detail.ChildNodes.Count ) ) { DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_FAILEDDESERIALIZATION" ) ); // // DispositionReport.ThrowFinal will close the HTTP stream so there is no point going on in this method // return; } // // If the original request was v1, then we'll need to // remap the output to use the v1 namespace. // if( 1 == Context.ApiVersionMajor || 2 == Context.ApiVersionMajor ) { newStream.Position = 0; TextReader reader = new StreamReader( newStream ); TextWriter writer = new StreamWriter( oldStream, new System.Text.UTF8Encoding( false ) ); string xml = reader.ReadToEnd(); // // We don't have to use the same 'loose' replacement as we did on the incoming request // because our response will be serialized such that the default namespace is our UDDI // namespace. // if( 1 == Context.ApiVersionMajor ) { xml = xml.Replace( "xmlns=\"urn:uddi-org:api_v2\"", "xmlns=\"urn:uddi-org:api\"" ); xml = xml.Replace( "generic=\"2.0\"", "generic=\"1.0\"" ); } writer.Write( xml ); writer.Flush(); } break; default: throw new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_UNKNOWNEXTSTAGE" ); } } catch( Exception e ) { DispositionReport.Throw( e ); } } public override Stream ChainStream( Stream stream ) { oldStream = stream; newStream = new MemoryStream(); return newStream; } private void CheckSOAPHeaders( SoapMessage message ) { // We want to check the following: // // - no SOAP Actor attribute exists // - no SOAP headers can have a must_understand attribute set to true // // Go through each header in our message // foreach( SoapHeader header in message.Headers ) { if( header.MustUnderstand ) { // // No headers can have this attribute set. // DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_SOAP_MUSTUNDERSTANDATT" ) ); return; } if( header.Actor.Length > 0 ) { // // Can't have a SOAP Actor attribute set, generate a SOAP fault with // no detail element and a 'Client' fault code // DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_SOAP_ACTORATT" ) ); return; } } } // // TODO: see if there is a way to better modularize this method and rename it. // private void CheckForSingleRequest( Stream stream ) { try { // // Move to the start of our stream // stream.Position = 0; XmlTextReader requestReader = new XmlTextReader( oldStream ); requestReader.MoveToContent(); // // TODO: should not hard-code SOAP names and namespaces // // // Move to the beginning of the SOAP envelope // requestReader.ReadStartElement( "Envelope", "http://schemas.xmlsoap.org/soap/envelope/" ); // // Move to the SOAP body // while( !requestReader.IsStartElement( "Body", "http://schemas.xmlsoap.org/soap/envelope/" ) && !requestReader.EOF ) { requestReader.Skip(); } // // Advance the current node to the first child of Body. This is presumably the UDDI message // requestReader.ReadStartElement( "Body", "http://schemas.xmlsoap.org/soap/envelope/" ); requestReader.MoveToContent(); // // This element MUST have a UDDI namespace // string uddiNamespace = requestReader.LookupNamespace( requestReader.Prefix ); switch( uddiNamespace ) { case "urn:uddi-org:api": { Context.ApiVersionMajor = 1; break; } case "urn:uddi-org:api_v2": { Context.ApiVersionMajor = 2; break; } case "urn:uddi-microsoft-com:api_v2_extensions": { Context.ApiVersionMajor = 2; break; } case "urn:uddi-org:repl": { Context.ApiVersionMajor = 2; break; } default: { // // This is a problem, we don't have a UDDI namespace. Throw an exception and get out of here. The // exception will be caught in our outer catch and sent to our client using DispositionReport.ThrowFinal. // throw new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_MISSINGUDDINS" ); } } // // Skip the children of this node // requestReader.Skip(); requestReader.MoveToContent(); // // Reset our stream so someone else can use it. // stream.Position = 0; // // If we are not at the end of the Body tag, then we have multiple requests, we should reject the message. // if( false == requestReader.LocalName.Equals( "Body" ) ) { DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_SOAP_MULTIPLEREQUEST" ) ); } } catch( UDDIException uddiException ) { DispositionReport.ThrowFinal( uddiException ); } catch { // // We'll get this exception if the message contains any invalid elements // DispositionReport.ThrowFinal( new UDDIException( ErrorType.E_fatalError, "UDDI_ERROR_FATALERROR_SOAP_INVALIDELEMENT" ) ); } } } }