dnSpy 實戰 - WebClient Header 改變之謎徹底解密
2 |
昨天分享重複使用 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/