How to do HTTPS with TcpClient just like HttpWebRe

2020-04-15 12:48发布

问题:

I've got a communication system based on TcpClient, and it works great except for when it's doing HTTPS to a particular IP. Then it starts to fail.

By using a browser or HttpWebRequest, I have no problems doing HTTPS to that IP.

I've created a test program to narrow my problem down to its basic essence, you can have a look at it here if you want: TestViaTcp

That test program works perfectly for basic HTTP to the same IP, it always produces a successful response to the request. I put it in a loop, trigger it with a keypress, it will continue to succeed all day long. As soon as I toggle the HTTPS, I get a recurring pattern. It'll work, then it won't, success followed by failure followed by success back and forth all day long.

The particular failure I keep getting is this one:

{"Authentication failed because the remote party has closed the transport stream."}
    [System.IO.IOException]: {"Authentication failed because the remote party has closed the transport stream."}
    Data: {System.Collections.ListDictionaryInternal}
    HelpLink: null
    InnerException: null
    Message: "Authentication failed because the remote party has closed the transport stream."
    Source: "System"
    TargetSite: {Void StartReadFrame(Byte[], Int32, System.Net.AsyncProtocolRequest)}

And here's the stack trace attached to that:

   at System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
   at System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
   at System.Net.Security.SslStream.AuthenticateAsClient(String targetHost, X509CertificateCollection clientCertificates, SslProtocols enabledSslProtocols, Boolean checkCertificateRevocation)
   at DeriveClassNameSpace.Services.Web.TcpMessaging.TestViaTcp(IPEndPoint endpoint, String auth, Boolean useSSL)

HttpWebRequest and the browser are both (IIRC) using the Win32 libraries to handle the back-and-forth communication, while TcpClient is (AFAIK) using the managed .net Socket class, so I'm sure there's a large difference between them. I do need to do this with TcpClient, so unfortunately I can't just "use HttpWebRequest since I know I can make it work".

The biggest hint as to what the problem is here is likely the "works, doesn't, works, doesn't" pattern, what's causing that? What can I do to avoid the IOException I'm getting? Is there some way to get the "always works" behaviour that I can see when I do the HTTPS with HttpWebRequest?

There should be something I can do with TcpClient to get it to act and react just like HttpWebRequest does, but I'm not there yet. Any ideas?

Note: The server I'm communicating with is configurable as to what port it listens on and what protocol it expects, but is otherwise completely unmodifiable.

Also note: I've read that .net 3.5 had this particular issue with SslStream before SP1, but I've got SP1 and my program is built targetting 3.5 so I'm assuming that this isn't a 'known bug' I'm running into here.

回答1:

Wouldn't you know it, after I spend the time forming the question is when I stumble upon the answer.

Here's the relevant documentation: jpsanders blog entry

The important part was this:

If the stack from the exception includes something similar to this: System.IO.IOException: Authentication failed because the remote party has closed the transport stream. It is possible that the server is an older server does not understand TLS and so you need to change this as specified in the 915599 kb to something like this: ServicePointManager.SecurityProtocol= SecurityProtocolType.Ssl3;Before you make any HTTPS calls in your application.

So I change my accepted protocols to this: (removing the possibility of TLS)

SslProtocols protocol = SslProtocols.Ssl2 | SslProtocols.Ssl3;

And everything works great.
I was allowing TLS, so it tries that first, and the server doesn't support it, so the stream is closed. The next time it uses Ssl2 or Ssl3 and everything's fine. Or something like that. It works, I'm a happy panda.