昨天分享重複使用 WebClient 時 Headers 會變動的踩雷經驗,陸續有讀者提問,歸納疑惑點不外乎「在哪些情況下哪些 Header 會改變?」,解答就藏在 .NET 的原始碼裡,如果你能找對位置的話。

要得到真相得追進 .NET Framework 或 .NET 6 原始碼,微軟有提供 .NET Framework 主要組件的原始碼(延伸閱讀:.NET Framework 3.5 SP1 原始碼已開放參考檢視 by 保哥在 Visual Studio 2010 如何逐步執行偵錯 .NET 核心原始碼 by 保哥),甚至 Visual Studio 針對第三方程式庫也可透過 Source Link 及 Decompile Source Code 功能深入偵察,而 .NET 6 更是完全開源任你研究。我個人則偏好 .NET 除錯神器 - dnSpy,不管有沒有官方原始碼,設中斷點按 F11 深入,同一招式通吃 .NET Framework 及第三方程式庫。

因此,我就用這次的案例溫習 dnSpy 恢復手感,以下是解析過程。

先寫一個 .NET 4.x 版 fxclient.cs,用 C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc -platform:x86 fxclient.cs 編譯成 fxclient.exe 當成分析對象:(延伸閱讀:野外求生系列 - 徒手製作 .NET .exe)

using System;
using System.Net;

namespace fxclient
{
    public class Program
    {
        [STAThread]
        public static void Main(string[] args)
        {
            var wc = new WebClient();
            wc.UseDefaultCredentials = true;
            wc.Headers.Add("Accept", "*/*");
            wc.Headers.Add("Content-Type", "application/json");
            wc.Headers.Add("X-ApiKey", "Secret");
            var url = "http://localhost/aspnet/postjson/";
            var res = wc.UploadString(url, @"{ ""x"": ""1"", ""y"": ""2"" }");
            Console.WriteLine(res);
            res = wc.UploadString(url, @"{ ""x"": ""3"", ""y"": ""4"" }");
            Console.WriteLine(res);
        }
    }
}

啟動 dnSpy-x86.exe,選取 fxclient.exe,在第 15 行 wc.Headers.Add("Accept", "/"); 設下中斷點:

用 F11 我追進 WebHeaderCollection.Add() 再設定中斷點。

整個程式執行過程我觀察到它被觸發四次,傳入 Header 名稱分別為 Accept、Content-Type、X-ApiKey 以及 X-ApiKey,答案已呼之欲出。

前三次的 Call Stack 為

System.dll!System.Net.WebHeaderCollection.Add(string name, string value) (IL=0x0000, Native=0x056B3BE8+0x2C)  
fxclient.exe!fxclient.Program.Main(string[] args) (IL=0x0050, Native=0x055E2B90+0xD2)

第四次則為

System.dll!System.Net.WebHeaderCollection.Add(string name, string value) (IL=0x0000, Native=0x056B3BE8+0x2C)  
System.dll!System.Net.HttpWebRequest.Headers.set(System.Net.WebHeaderCollection value) (IL=0x0041, Native=0x065DAED8+0x117)  
System.dll!System.Net.WebClient.CopyHeadersTo(System.Net.WebRequest request) (IL=0x010C, Native=0x065DA390+0x1D2)
System.dll!System.Net.WebClient.GetWebRequest(System.Uri address) (IL=0x000E, Native=0x01579058+0x4B)
System.dll!System.Net.WebClient.UploadDataInternal(System.Uri address, string method, byte[] data, out System.Net.WebRequest request) (IL≈0x0020, Native=0x01578E78+0x92)
System.dll!System.Net.WebClient.UploadString(System.Uri address, string method, string data) (IL≈0x0064, Native=0x010688B8+0x166)
System.dll!System.Net.WebClient.UploadString(string address, string data) (IL≈0x0016, Native=0x056B7350+0x89)
fxclient.exe!fxclient.Program.Main(string[] args) (IL≈0x0057, Native=0x055E2B90+0xEF)

依照這些線索再深入追查,WebClient 在 UploadString()、UploadData() 時會呼叫 UpdateDataInternal()、UploadValues() 呼叫 UploadValuesInternal()、UploadFile() 呼叫 UploadFileInternal()。UploadValuesInternal() 用 this.m_headers["Content-Type"] = "application/x-www-form-urlencoded" 覆寫 Content-Type;UploadFileInternal() 則是預設 "application/octet-stream" 但允許 "multipart/*"。至於 UpdateDataInternal() 則不調整 Content-Type 內容。但幾乎所有 Upload*、Download* 方法都會呼叫 GetWebRequest(Uri address) 建立 WebRequest,其中有個 CopyHeadersTo() 將 WebClient 的 Headers 複製給 WebRequest:

protected virtual WebRequest GetWebRequest(Uri address)
{
	WebRequest webRequest = WebRequest.Create(address);
	this.CopyHeadersTo(webRequest);
	//...略...

而 CopyHeadersTo() 長這樣:

private void CopyHeadersTo(WebRequest request)
{
	if (this.m_headers != null && request is HttpWebRequest)
	{
		string text = this.m_headers["Accept"];
		string text2 = this.m_headers["Connection"];
		string text3 = this.m_headers["Content-Type"];
		string text4 = this.m_headers["Expect"];
		string text5 = this.m_headers["Referer"];
		string text6 = this.m_headers["User-Agent"];
		string text7 = this.m_headers["Host"];
		this.m_headers.RemoveInternal("Accept");
		this.m_headers.RemoveInternal("Connection");
		this.m_headers.RemoveInternal("Content-Type");
		this.m_headers.RemoveInternal("Expect");
		this.m_headers.RemoveInternal("Referer");
		this.m_headers.RemoveInternal("User-Agent");
		this.m_headers.RemoveInternal("Host");
		request.Headers = this.m_headers; //這是第四次 WebHeaderCollection.Add() 被觸發的原因
		if (text != null && text.Length > 0)
		{
			((HttpWebRequest)request).Accept = text;
		}
		if (text2 != null && text2.Length > 0)
		{
			((HttpWebRequest)request).Connection = text2;
		}
		if (text3 != null && text3.Length > 0)
		{
			((HttpWebRequest)request).ContentType = text3;
		}
		if (text4 != null && text4.Length > 0)
		{
			((HttpWebRequest)request).Expect = text4;
		}
		if (text5 != null && text5.Length > 0)
		{
			((HttpWebRequest)request).Referer = text5;
		}
		if (text6 != null && text6.Length > 0)
		{
			((HttpWebRequest)request).UserAgent = text6;
		}
		if (!string.IsNullOrEmpty(text7))
		{
			((HttpWebRequest)request).Host = text7;
		}
	}
}

真相大白,WebClient 每次複製完 Accept、Connection、Content-Type、Expect、Referer、User-Agent、Host 等 Header,會將其自 Headers 清除,這就是 WebClient 重複使用 Accept 及 Content-Type 會消失的原因。

  • 回覆 JerryH 的問題:
    UploadData 會固定覆寫 Content-Type 為 application/x-www-form-urlencoded,但其他 Header 一樣會被清除。
  • 回覆 Raye Li 的問題:
    Accept、Connection、Content-Type、Expect、Referer、User-Agent、Host 等 Header 在每次 Upload*() 或 Download*() 後會消失,依此原則決定哪些要重設。
    證明:一開始隨便加個 var download = wc.DownloadString("https://www.google.com"),第一次 UploadString() 也抓不到 x, y (因為 Content-Type 在 DownloadString() 後被清除了)

A example of how to use dnSpy to trace source code of .NET BCL.


Comments

# by 骨董修復菜鳥

請問黑大,因為我只有編譯後的程式檔案和網頁,可是那個檔案內嵌的JS程式碼有要修的設定,這個工具可用在已經先行編譯的網站上嗎?

# by Jeffrey

to 骨董修復菜鳥,請參考: https://blog.darkthread.net/blog/ildasm-change-embedded-resource/

Post a comment