某台透過 .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 的解法,請參考前文

        static void 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 讀取網站資料遲早要對類似問題,宜多加留意。


Comments

# by Alex

有考慮改用這樣的方法嗎? ServicePointManager.SecurityProtocol = GetSecurityProtocol(); private static SecurityProtocolType GetSecurityProtocol() { var result = 0; foreach(var value in Enum.GetValues(typeof(SecurityProtocolType))) result += (int) value; return (SecurityProtocolType) result; }

# by Jeffrey

to Alex, 這方法挺好,但因為涉及資安,個人覺得正向表列想開放的項目更安全一些。

Post a comment


83 - 79 =