某台透過 .NET WebClient 物件爬網頁抓資料排程忽然出現:
基礎連接已關閉: 傳送時發生未預期的錯誤。 ---> System.IO.IOException: 驗證失敗,因為遠端群體已經關閉傳輸資料流。
The underlying connection was closed: An unexpected error occurred on a send. ---> System.IO.IOException: Authentication failed because the remote party has closed the transport stream
有趣的是,上回同一排程就發生過類似狀況,原本早上還執行得好好的,近中午時開始出錯。二次出錯的時點幾乎相同,推測是該網站的固定換版上線時間。
對照測試,使用 IE 或 Chrome 開啟網頁正常,只有透過 WebClient 取回 HTML 內容時才出錯,在本機測試也發生相同錯誤。印出 Exception 內容如下:
ERROR=System.Net.WebException: 基礎連接已關閉: 傳送時發生未預期的錯誤。 ---> System.IO.IOException: 驗證失敗,因為遠端群體已經關閉傳輸資料流。
於 System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
於 System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
於 System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
於 System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
於 System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
於 System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
於 System.Net.TlsStream.CallProcessAuthentication(Object state)
於 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
於 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
於 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
於 System.Net.TlsStream.ProcessAuthentication(LazyAsyncResult result)
於 System.Net.TlsStream.Write(Byte[] buffer, Int32 offset, Int32 size)
於 System.Net.PooledStream.Write(Byte[] buffer, Int32 offset, Int32 size)
於 System.Net.ConnectStream.WriteHeaders(Boolean async)
--- 內部例外狀況堆疊追蹤的結尾 ---
於 System.Net.WebClient.DownloadDataInternal(Uri address, WebRequest& request)
於 System.Net.WebClient.DownloadString(Uri address)
於 System.Net.WebClient.DownloadString(String address)
錯誤訊息明確指向 SSL,爬文後恍然大悟,原來跟上回研究過的 TLS 1.0 停用議題有關,研判對方網站調整系統停用了較不安全的 TLS 1.0,依上回心得,.NET 客戶端使用 WebClient、WCF 以 HTTPS 連線遠端主機,也會涉及 TLS 1.0/1.1/1.2 版本議題,不同版本 .NET 的處理方式不同:
- .NET 4.6內建支援且預設使用 TLS 1.2
- .NET 4.5內建支援,但需透過 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 設為預設協定
- .NET 4本身不支援,但安裝 .NET 4.5 後即可使用 TLS 1.2,指定 TLS 1.2 的寫法為 ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
我的程式是 .NET 4.5,在調整 ServicePointManager.SecurityProtocol 設定擴大 SSL/TLS 版本範圍後,問題排除:(若改程式不方便,亦有修改 Registry 的解法,請參考前文)
staticvoid Main(string[] args)
{
try
{
WebClient wc = new WebClient();
//REF: https://stackoverflow.com/a/39534068/288936
ServicePointManager.SecurityProtocol =
SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls |
SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
string res = wc.DownloadString("httqs://some-server/test");
Console.WriteLine(res);
}
catch (Exception ex)
{
Console.WriteLine("ERROR=" + ex.ToString());
}
Console.ReadLine();
}
TLS 1.0 已被視為不安全,近期應會被各大網站陸續停用,使用 .NET 讀取網站資料遲早要對類似問題,宜多加留意。