_NegotiateClient.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / Net / System / Net / _NegotiateClient.cs / 1305376 / _NegotiateClient.cs

                            //------------------------------------------------------------------------------ 
// 
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// 
//----------------------------------------------------------------------------- 

namespace System.Net { 
    using System.Diagnostics; 
    using System.Collections;
    using System.Net.Sockets; 
    using System.Security.Authentication.ExtendedProtection;
    using System.Security.Permissions;
    using System.Globalization;
 
    internal class NegotiateClient : ISessionAuthenticationModule {
 
        internal const string AuthType = "Negotiate"; 
        private const string negotiateHeader = "Negotiate";
        private const string negotiateSignature = "negotiate"; 
        private const string nego2Header = "Nego2";
        private const string nego2Signature = "nego2";

        // we can't work on non-NT2K platforms or non Win, so we shut off, 
        // NOTE this exception IS caught internally.
        public NegotiateClient() { 
            if (!ComNetOS.IsWin2K) { 
                throw new PlatformNotSupportedException(SR.GetString(SR.Win2000Required));
            } 
        }

        public Authorization Authenticate(string challenge, WebRequest webRequest, ICredentials credentials) {
            GlobalLog.Print("NegotiateClient::Authenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()"); 
            return DoAuthenticate(challenge, webRequest, credentials, false);
        } 
 
        private Authorization DoAuthenticate(string challenge, WebRequest webRequest, ICredentials credentials, bool preAuthenticate) {
            GlobalLog.Print("NegotiateClient::DoAuthenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " preAuthenticate:" + preAuthenticate.ToString()); 

            GlobalLog.Assert(credentials != null, "NegotiateClient::DoAuthenticate()|credentials == null");
            if (credentials == null) {
                return null; 
            }
 
            HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; 

            GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::DoAuthenticate()|httpWebRequest == null"); 
            GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "NegotiateClient::DoAuthenticate()|httpWebRequest.ChallengedUri == null");

            NTAuthentication authSession = null;
            string incoming = null; 
            bool useNego2 = false; // In case of pre-auth we always use "Negotiate", never "Nego2".
 
            if (!preAuthenticate) { 
                int index = GetSignatureIndex(challenge, out useNego2);
                if (index < 0) { 
                    return null;
                }

                int blobBegin = index + (useNego2 ? nego2Signature.Length : negotiateSignature.Length); 

                // 
                // there may be multiple challenges. If the next character after the 
                // package name is not a comma then it is challenge data
                // 

                if (challenge.Length > blobBegin && challenge[blobBegin] != ',') {
                    ++blobBegin;
                } 
                else {
                    index = -1; 
                } 

                if (index >= 0 && challenge.Length > blobBegin) 
                {
                    // Strip other modules information in case of multiple challenges
                    // i.e do not take ", NTLM" as part of the following Negotiate blob
                    // Negotiate TlRMTVNTUAACAAAADgAOADgAAAA1wo ... MAbwBmAHQALgBjAG8AbQAAAAAA,NTLM 
                    index = challenge.IndexOf(',', blobBegin);
                    if (index != -1) 
                        incoming = challenge.Substring(blobBegin, index - blobBegin); 
                    else
                        incoming = challenge.Substring(blobBegin); 
                }

                authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this);
                GlobalLog.Print("NegotiateClient::DoAuthenticate() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession)); 
            }
 
            if (authSession==null) 
            {
                // Credentials are always set for "Negotiate", never for "Nego2". A customer shouldn't even know 
                // about "Nego2".
                NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, negotiateSignature);
                GlobalLog.Print("NegotiateClient::DoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC));
 
                string username = string.Empty;
                if (NC == null || (!(NC is SystemNetworkCredential) && (username = NC.InternalGetUserName()).Length == 0)) 
                { 
                    return null;
                } 

                ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
                if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this))
                    return null; 

                string spn = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest); 
                GlobalLog.Print("NegotiateClient::Authenticate() ChallengedSpn:" + ValidationHelper.ToString(spn)); 

                ChannelBinding binding = null; 
                if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
                {
                    binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
                } 

                authSession = 
                    new NTAuthentication( 
                        AuthType,
                        NC, 
                        spn,
                        httpWebRequest,
                        binding);
 
                GlobalLog.Print("NegotiateClient::DoAuthenticate() setting SecurityContext for:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " to authSession:" + ValidationHelper.HashString(authSession));
                httpWebRequest.CurrentAuthenticationState.SetSecurityContext(authSession, this); 
            } 

            string clientResponse = authSession.GetOutgoingBlob(incoming); 
            if (clientResponse==null) {
                return null;
            }
 
            bool canShareConnection = httpWebRequest.UnsafeOrProxyAuthenticatedConnectionSharing;
            if (canShareConnection) { 
                httpWebRequest.LockConnection = true; 
            }
 
            // this is the first leg of an NTLM handshake,
            // set the NtlmKeepAlive override *STRICTLY* only in this case.
            httpWebRequest.NtlmKeepAlive = incoming==null && authSession.IsValidContext && !authSession.IsKerberos;
 
            // If we received a "Nego2" header value from the server, we'll respond with "Nego2" in the "Authorization"
            // header. If the server sent a "Negotiate" header value or if pre-authenticate is used (i.e. the auth blob 
            // is sent with the first request), we send "Negotiate" in the "Authorization" header. 
            return AuthenticationManager.GetGroupAuthorization(this, (useNego2 ? nego2Header : negotiateHeader) +
                " " + clientResponse, authSession.IsCompleted, authSession, canShareConnection, authSession.IsKerberos); 
        }

        public bool CanPreAuthenticate {
            get { 
                return true;
            } 
        } 

        public Authorization PreAuthenticate(WebRequest webRequest, ICredentials credentials) { 
            GlobalLog.Print("NegotiateClient::PreAuthenticate() webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()");
            return DoAuthenticate(null, webRequest, credentials, true);
        }
 
        public string AuthenticationType {
            get { 
                return AuthType; 
            }
        } 

        //
        // called when getting the final blob on the 200 OK from the server
        // 
        public bool Update(string challenge, WebRequest webRequest) {
            GlobalLog.Print("NegotiateClient::Update(): " + challenge); 
 
            HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
 
            GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::Update()|httpWebRequest == null");
            GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "NegotiateClient::Update()|httpWebRequest.ChallengedUri == null");

            // 
            // try to retrieve the state of the ongoing handshake
            // 
 
            NTAuthentication authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this);
            GlobalLog.Print("NegotiateClient::Update() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession)); 

            if (authSession==null) {
                GlobalLog.Print("NegotiateClient::Update() null session returning true");
                return true; 
            }
 
            GlobalLog.Print("NegotiateClient::Update() authSession.IsCompleted:" + authSession.IsCompleted.ToString()); 

            if (!authSession.IsCompleted && httpWebRequest.CurrentAuthenticationState.StatusCodeMatch==httpWebRequest.ResponseStatusCode) { 
                GlobalLog.Print("NegotiateClient::Update() still handshaking (based on status code) returning false");
                return false;
            }
 
            // now possibly close the ConnectionGroup after authentication is done.
            if (!httpWebRequest.UnsafeOrProxyAuthenticatedConnectionSharing) { 
                GlobalLog.Print("NegotiateClient::Update() releasing ConnectionGroup:" + httpWebRequest.GetConnectionGroupLine()); 
                httpWebRequest.ServicePoint.ReleaseConnectionGroup(httpWebRequest.GetConnectionGroupLine());
            } 

            //
            // the whole point here is to close the Security Context (this will complete the authentication handshake
            // with server authentication for schemese that support it such as Kerberos) 
            //
            bool useNego2 = true; 
            int index = challenge==null ? -1 : GetSignatureIndex(challenge, out useNego2); 
            if (index>=0) {
                int blobBegin = index + (useNego2 ? nego2Signature.Length : negotiateSignature.Length); 
                string incoming = null;

                //
                // there may be multiple challenges. If the next character after the 
                // package name is not a comma then it is challenge data
                // 
                if (challenge.Length > blobBegin && challenge[blobBegin] != ',') { 
                    ++blobBegin;
                } else { 
                    index = -1;
                }
                if (index >= 0 && challenge.Length > blobBegin) {
                    incoming = challenge.Substring(blobBegin); 
                }
                GlobalLog.Print("NegotiateClient::Update() this must be a final incoming blob:[" + ValidationHelper.ToString(incoming) + "]"); 
                string clientResponse = authSession.GetOutgoingBlob(incoming); 
                httpWebRequest.CurrentAuthenticationState.Authorization.MutuallyAuthenticated = authSession.IsMutualAuthFlag;
                GlobalLog.Print("NegotiateClient::Update() GetOutgoingBlob() returns clientResponse:[" + ValidationHelper.ToString(clientResponse) + "] IsCompleted:" + authSession.IsCompleted.ToString()); 
            }

            // Extract the CBT we used and cache it for future requests that want to do preauth
            httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, authSession.ChannelBinding); 

            GlobalLog.Print("NegotiateClient::Update() session removed and ConnectionGroup released returning true"); 
            ClearSession(httpWebRequest); 
            return true;
        } 

        public void ClearSession(WebRequest webRequest) {
            HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
            GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::ClearSession()|httpWebRequest == null"); 
            httpWebRequest.CurrentAuthenticationState.ClearSession();
        } 
 
        public bool CanUseDefaultCredentials {
            get { 
                return true;
            }
        }
 
        private static int GetSignatureIndex(string challenge, out bool useNego2) {
            // Negotiate supports two header fields "Nego2" and "Negotiate". If we find "Nego2" we use it, 
            // otherwise we fall back to "Negotiate" (if available). 
            useNego2 = true;
 
            int index = -1;

            // Consider Nego2 headers only on Win7 and later. Older OS version don't support LiveSSP.
            if (ComNetOS.IsWin7) { 
                index = AuthenticationManager.FindSubstringNotInQuotes(challenge, nego2Signature);
            } 
 
            if (index < 0) {
                useNego2 = false; 
                index = AuthenticationManager.FindSubstringNotInQuotes(challenge, negotiateSignature);
            }
            return index;
        } 
    }; // class NegotiateClient
 
 
} // namespace System.Net

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
//------------------------------------------------------------------------------ 
// 
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// 
//----------------------------------------------------------------------------- 

namespace System.Net { 
    using System.Diagnostics; 
    using System.Collections;
    using System.Net.Sockets; 
    using System.Security.Authentication.ExtendedProtection;
    using System.Security.Permissions;
    using System.Globalization;
 
    internal class NegotiateClient : ISessionAuthenticationModule {
 
        internal const string AuthType = "Negotiate"; 
        private const string negotiateHeader = "Negotiate";
        private const string negotiateSignature = "negotiate"; 
        private const string nego2Header = "Nego2";
        private const string nego2Signature = "nego2";

        // we can't work on non-NT2K platforms or non Win, so we shut off, 
        // NOTE this exception IS caught internally.
        public NegotiateClient() { 
            if (!ComNetOS.IsWin2K) { 
                throw new PlatformNotSupportedException(SR.GetString(SR.Win2000Required));
            } 
        }

        public Authorization Authenticate(string challenge, WebRequest webRequest, ICredentials credentials) {
            GlobalLog.Print("NegotiateClient::Authenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()"); 
            return DoAuthenticate(challenge, webRequest, credentials, false);
        } 
 
        private Authorization DoAuthenticate(string challenge, WebRequest webRequest, ICredentials credentials, bool preAuthenticate) {
            GlobalLog.Print("NegotiateClient::DoAuthenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " preAuthenticate:" + preAuthenticate.ToString()); 

            GlobalLog.Assert(credentials != null, "NegotiateClient::DoAuthenticate()|credentials == null");
            if (credentials == null) {
                return null; 
            }
 
            HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; 

            GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::DoAuthenticate()|httpWebRequest == null"); 
            GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "NegotiateClient::DoAuthenticate()|httpWebRequest.ChallengedUri == null");

            NTAuthentication authSession = null;
            string incoming = null; 
            bool useNego2 = false; // In case of pre-auth we always use "Negotiate", never "Nego2".
 
            if (!preAuthenticate) { 
                int index = GetSignatureIndex(challenge, out useNego2);
                if (index < 0) { 
                    return null;
                }

                int blobBegin = index + (useNego2 ? nego2Signature.Length : negotiateSignature.Length); 

                // 
                // there may be multiple challenges. If the next character after the 
                // package name is not a comma then it is challenge data
                // 

                if (challenge.Length > blobBegin && challenge[blobBegin] != ',') {
                    ++blobBegin;
                } 
                else {
                    index = -1; 
                } 

                if (index >= 0 && challenge.Length > blobBegin) 
                {
                    // Strip other modules information in case of multiple challenges
                    // i.e do not take ", NTLM" as part of the following Negotiate blob
                    // Negotiate TlRMTVNTUAACAAAADgAOADgAAAA1wo ... MAbwBmAHQALgBjAG8AbQAAAAAA,NTLM 
                    index = challenge.IndexOf(',', blobBegin);
                    if (index != -1) 
                        incoming = challenge.Substring(blobBegin, index - blobBegin); 
                    else
                        incoming = challenge.Substring(blobBegin); 
                }

                authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this);
                GlobalLog.Print("NegotiateClient::DoAuthenticate() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession)); 
            }
 
            if (authSession==null) 
            {
                // Credentials are always set for "Negotiate", never for "Nego2". A customer shouldn't even know 
                // about "Nego2".
                NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, negotiateSignature);
                GlobalLog.Print("NegotiateClient::DoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC));
 
                string username = string.Empty;
                if (NC == null || (!(NC is SystemNetworkCredential) && (username = NC.InternalGetUserName()).Length == 0)) 
                { 
                    return null;
                } 

                ICredentialPolicy policy = AuthenticationManager.CredentialPolicy;
                if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this))
                    return null; 

                string spn = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest); 
                GlobalLog.Print("NegotiateClient::Authenticate() ChallengedSpn:" + ValidationHelper.ToString(spn)); 

                ChannelBinding binding = null; 
                if (httpWebRequest.CurrentAuthenticationState.TransportContext != null)
                {
                    binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint);
                } 

                authSession = 
                    new NTAuthentication( 
                        AuthType,
                        NC, 
                        spn,
                        httpWebRequest,
                        binding);
 
                GlobalLog.Print("NegotiateClient::DoAuthenticate() setting SecurityContext for:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " to authSession:" + ValidationHelper.HashString(authSession));
                httpWebRequest.CurrentAuthenticationState.SetSecurityContext(authSession, this); 
            } 

            string clientResponse = authSession.GetOutgoingBlob(incoming); 
            if (clientResponse==null) {
                return null;
            }
 
            bool canShareConnection = httpWebRequest.UnsafeOrProxyAuthenticatedConnectionSharing;
            if (canShareConnection) { 
                httpWebRequest.LockConnection = true; 
            }
 
            // this is the first leg of an NTLM handshake,
            // set the NtlmKeepAlive override *STRICTLY* only in this case.
            httpWebRequest.NtlmKeepAlive = incoming==null && authSession.IsValidContext && !authSession.IsKerberos;
 
            // If we received a "Nego2" header value from the server, we'll respond with "Nego2" in the "Authorization"
            // header. If the server sent a "Negotiate" header value or if pre-authenticate is used (i.e. the auth blob 
            // is sent with the first request), we send "Negotiate" in the "Authorization" header. 
            return AuthenticationManager.GetGroupAuthorization(this, (useNego2 ? nego2Header : negotiateHeader) +
                " " + clientResponse, authSession.IsCompleted, authSession, canShareConnection, authSession.IsKerberos); 
        }

        public bool CanPreAuthenticate {
            get { 
                return true;
            } 
        } 

        public Authorization PreAuthenticate(WebRequest webRequest, ICredentials credentials) { 
            GlobalLog.Print("NegotiateClient::PreAuthenticate() webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()");
            return DoAuthenticate(null, webRequest, credentials, true);
        }
 
        public string AuthenticationType {
            get { 
                return AuthType; 
            }
        } 

        //
        // called when getting the final blob on the 200 OK from the server
        // 
        public bool Update(string challenge, WebRequest webRequest) {
            GlobalLog.Print("NegotiateClient::Update(): " + challenge); 
 
            HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
 
            GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::Update()|httpWebRequest == null");
            GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "NegotiateClient::Update()|httpWebRequest.ChallengedUri == null");

            // 
            // try to retrieve the state of the ongoing handshake
            // 
 
            NTAuthentication authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this);
            GlobalLog.Print("NegotiateClient::Update() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession)); 

            if (authSession==null) {
                GlobalLog.Print("NegotiateClient::Update() null session returning true");
                return true; 
            }
 
            GlobalLog.Print("NegotiateClient::Update() authSession.IsCompleted:" + authSession.IsCompleted.ToString()); 

            if (!authSession.IsCompleted && httpWebRequest.CurrentAuthenticationState.StatusCodeMatch==httpWebRequest.ResponseStatusCode) { 
                GlobalLog.Print("NegotiateClient::Update() still handshaking (based on status code) returning false");
                return false;
            }
 
            // now possibly close the ConnectionGroup after authentication is done.
            if (!httpWebRequest.UnsafeOrProxyAuthenticatedConnectionSharing) { 
                GlobalLog.Print("NegotiateClient::Update() releasing ConnectionGroup:" + httpWebRequest.GetConnectionGroupLine()); 
                httpWebRequest.ServicePoint.ReleaseConnectionGroup(httpWebRequest.GetConnectionGroupLine());
            } 

            //
            // the whole point here is to close the Security Context (this will complete the authentication handshake
            // with server authentication for schemese that support it such as Kerberos) 
            //
            bool useNego2 = true; 
            int index = challenge==null ? -1 : GetSignatureIndex(challenge, out useNego2); 
            if (index>=0) {
                int blobBegin = index + (useNego2 ? nego2Signature.Length : negotiateSignature.Length); 
                string incoming = null;

                //
                // there may be multiple challenges. If the next character after the 
                // package name is not a comma then it is challenge data
                // 
                if (challenge.Length > blobBegin && challenge[blobBegin] != ',') { 
                    ++blobBegin;
                } else { 
                    index = -1;
                }
                if (index >= 0 && challenge.Length > blobBegin) {
                    incoming = challenge.Substring(blobBegin); 
                }
                GlobalLog.Print("NegotiateClient::Update() this must be a final incoming blob:[" + ValidationHelper.ToString(incoming) + "]"); 
                string clientResponse = authSession.GetOutgoingBlob(incoming); 
                httpWebRequest.CurrentAuthenticationState.Authorization.MutuallyAuthenticated = authSession.IsMutualAuthFlag;
                GlobalLog.Print("NegotiateClient::Update() GetOutgoingBlob() returns clientResponse:[" + ValidationHelper.ToString(clientResponse) + "] IsCompleted:" + authSession.IsCompleted.ToString()); 
            }

            // Extract the CBT we used and cache it for future requests that want to do preauth
            httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, authSession.ChannelBinding); 

            GlobalLog.Print("NegotiateClient::Update() session removed and ConnectionGroup released returning true"); 
            ClearSession(httpWebRequest); 
            return true;
        } 

        public void ClearSession(WebRequest webRequest) {
            HttpWebRequest httpWebRequest = webRequest as HttpWebRequest;
            GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::ClearSession()|httpWebRequest == null"); 
            httpWebRequest.CurrentAuthenticationState.ClearSession();
        } 
 
        public bool CanUseDefaultCredentials {
            get { 
                return true;
            }
        }
 
        private static int GetSignatureIndex(string challenge, out bool useNego2) {
            // Negotiate supports two header fields "Nego2" and "Negotiate". If we find "Nego2" we use it, 
            // otherwise we fall back to "Negotiate" (if available). 
            useNego2 = true;
 
            int index = -1;

            // Consider Nego2 headers only on Win7 and later. Older OS version don't support LiveSSP.
            if (ComNetOS.IsWin7) { 
                index = AuthenticationManager.FindSubstringNotInQuotes(challenge, nego2Signature);
            } 
 
            if (index < 0) {
                useNego2 = false; 
                index = AuthenticationManager.FindSubstringNotInQuotes(challenge, negotiateSignature);
            }
            return index;
        } 
    }; // class NegotiateClient
 
 
} // namespace System.Net

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.

                        

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK