1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . IO ;
4+ using System . Linq ;
5+ using System . Net ;
6+ using System . Net . Http ;
7+ using System . Text ;
8+ using System . Text . Json ;
9+ using System . Threading ;
10+ using System . Threading . Tasks ;
11+ using Serilog ;
12+
13+ namespace ShadowPluginLoader . WinUI . Helpers ;
14+
15+ /// <summary>
16+ /// HttpHelper
17+ /// </summary>
18+ public class BaseHttpHelper
19+ {
20+ /// <summary>
21+ /// Lazy, thread-safe singleton instance
22+ /// </summary>
23+ private static readonly Lazy < BaseHttpHelper > InnerInstance = new ( ( ) => new BaseHttpHelper ( ) , LazyThreadSafetyMode . ExecutionAndPublication ) ;
24+
25+ /// <summary>
26+ /// 获取单例实例(线程安全、惰性初始化)
27+ /// </summary>
28+ public static BaseHttpHelper Instance => InnerInstance . Value ;
29+
30+ /// <summary>
31+ /// 私有构造函数,防止外部直接实例化
32+ /// </summary>
33+ protected BaseHttpHelper ( )
34+ {
35+ // 初始化 HttpClient(默认不使用代理)
36+ Client = new HttpClient ( new HttpClientHandler ( ) ) { Timeout = TimeSpan . FromSeconds ( 100 ) } ;
37+ }
38+
39+ /// <summary>
40+ /// HttpClient 可以在运行时被替换(例如切换代理),因此不是 readonly
41+ /// </summary>
42+ protected HttpClient Client ;
43+ private readonly object _clientLock = new ( ) ;
44+ /// <summary>
45+ /// Proxy
46+ /// </summary>
47+ protected IWebProxy ? Proxy ;
48+ private readonly JsonSerializerOptions _jsonOptions = new ( ) { PropertyNameCaseInsensitive = true } ;
49+
50+ /// <summary>
51+ /// 创建HttpRequestMessage
52+ /// </summary>
53+ /// <param name="method"></param>
54+ /// <param name="url"></param>
55+ /// <param name="headers"></param>
56+ /// <returns></returns>
57+ public HttpRequestMessage CreateRequestMessage ( HttpMethod method , string url ,
58+ Dictionary < string , string > ? headers = null )
59+ {
60+ var httpRequestMessage = new HttpRequestMessage ( method , url ) ;
61+ // 默认接受 JSON
62+ httpRequestMessage . Headers . Accept . Clear ( ) ;
63+ httpRequestMessage . Headers . Accept . Add ( new System . Net . Http . Headers . MediaTypeWithQualityHeaderValue ( "application/json" ) ) ;
64+
65+ if ( headers == null ) return httpRequestMessage ;
66+ foreach ( var header in headers . Where ( header => ! string . IsNullOrEmpty ( header . Key ) && ! string . IsNullOrEmpty ( header . Value ) ) )
67+ {
68+ try
69+ {
70+ httpRequestMessage . Headers . Add ( header . Key , header . Value ) ;
71+ }
72+ catch
73+ {
74+ // 忽略无法添加为 Header 的键(例如 Content headers),调用方应通过 Content 设置
75+ }
76+ }
77+
78+ return httpRequestMessage ;
79+ }
80+
81+ /// <summary>
82+ /// GET 并将 JSON 反序列化为 T
83+ /// </summary>
84+ public async Task < T ? > GetAsync < T > ( string url , Dictionary < string , string > ? query = null , Dictionary < string , string > ? headers = null , CancellationToken cancellationToken = default )
85+ {
86+ try
87+ {
88+ // 支持可选的 query 参数
89+ var finalUrl = AppendQueryToUrl ( url , query ) ;
90+ using var request = CreateRequestMessage ( HttpMethod . Get , finalUrl , headers ) ;
91+ Log . Debug ( "[GET] Sending request to {Url}" , url ) ;
92+
93+ // 抓取当前 HttpClient 引用,避免在重置时发生竞争
94+ var client = Client ;
95+ using var response = await client . SendAsync ( request , cancellationToken ) ;
96+ var respText = await response . Content . ReadAsStringAsync ( cancellationToken ) ;
97+
98+ Log . Debug ( "[GET] Response from {Url}: {Resp}" , url , respText ) ;
99+ // 记录重要信息
100+ var respLen = respText . Length ;
101+ Log . Information ( "[GET] {Url} -> {StatusCode}, length={Len}" , url , response . StatusCode , respLen ) ;
102+
103+ response . EnsureSuccessStatusCode ( ) ;
104+
105+ // 如果响应为空,则返回默认值
106+ if ( string . IsNullOrWhiteSpace ( respText ) ) return default ;
107+ return JsonSerializer . Deserialize < T > ( respText , _jsonOptions ) ;
108+ }
109+ catch ( Exception ex )
110+ {
111+ Log . Error ( ex , "GetAsync failed for {Url}" , url ) ;
112+ throw ;
113+ }
114+ }
115+
116+ // 将查询参数正确追加到 URL(处理已有 query 并对键/值进行编码)
117+ private static string AppendQueryToUrl ( string url , Dictionary < string , string > ? query )
118+ {
119+ if ( query == null || query . Count == 0 ) return url ;
120+
121+ try
122+ {
123+ var ub = new UriBuilder ( url ) ;
124+ // ub.Query 带前导 '?', 去掉
125+ var existing = ub . Query ;
126+ if ( ! string . IsNullOrEmpty ( existing ) && existing . StartsWith ( "?" ) ) existing = existing . Substring ( 1 ) ;
127+
128+ var parts = new List < string > ( ) ;
129+ if ( ! string . IsNullOrEmpty ( existing ) ) parts . Add ( existing ) ;
130+ foreach ( var kv in query )
131+ {
132+ var k = Uri . EscapeDataString ( kv . Key ) ;
133+ var v = Uri . EscapeDataString ( kv . Value ) ;
134+ parts . Add ( $ "{ k } ={ v } ") ;
135+ }
136+
137+ ub . Query = string . Join ( "&" , parts ) ;
138+ return ub . Uri . ToString ( ) ;
139+ }
140+ catch
141+ {
142+ // fallback: 直接拼接
143+ var sb = new StringBuilder ( url ) ;
144+ sb . Append ( url . Contains ( "?" ) ? "&" : "?" ) ;
145+ sb . Append ( string . Join ( "&" , query . Select ( kv => $ "{ Uri . EscapeDataString ( kv . Key ) } ={ Uri . EscapeDataString ( kv . Value ) } ") ) ) ;
146+ return sb . ToString ( ) ;
147+ }
148+ }
149+
150+ /// <summary>
151+ /// POST JSON 并将响应反序列化为 TResponse
152+ /// </summary>
153+ public async Task < TResponse ? > PostJsonAsync < TRequest , TResponse > ( string url , TRequest payload , Dictionary < string , string > ? headers = null , CancellationToken cancellationToken = default )
154+ {
155+ try
156+ {
157+ using var request = CreateRequestMessage ( HttpMethod . Post , url , headers ) ;
158+ var json = JsonSerializer . Serialize ( payload , _jsonOptions ) ;
159+ request . Content = new StringContent ( json , Encoding . UTF8 , "application/json" ) ;
160+
161+ Log . Debug ( "[POST] Sending to {Url}, payload: {Payload}" , url , json ) ;
162+
163+ var client = Client ;
164+ using var response = await client . SendAsync ( request , cancellationToken ) ;
165+ var respText = await response . Content . ReadAsStringAsync ( cancellationToken ) ;
166+
167+ Log . Debug ( "[POST] Response from {Url}: {Resp}" , url , respText ) ;
168+ var respLen = respText . Length ;
169+ Log . Information ( "[POST] {Url} -> {StatusCode}, response-length={Len}" , url , response . StatusCode , respLen ) ;
170+
171+ response . EnsureSuccessStatusCode ( ) ;
172+
173+ if ( string . IsNullOrWhiteSpace ( respText ) ) return default ;
174+ return JsonSerializer . Deserialize < TResponse > ( respText , _jsonOptions ) ;
175+ }
176+ catch ( Exception ex )
177+ {
178+ Log . Error ( ex , "PostJsonAsync failed for {Url}" , url ) ;
179+ throw ;
180+ }
181+ }
182+
183+ /// <summary>
184+ /// 下载文件并返回字节数组
185+ /// </summary>
186+ public async Task < byte [ ] > GetFileAsync ( string url , Dictionary < string , string > ? headers = null , CancellationToken cancellationToken = default )
187+ {
188+ try
189+ {
190+ using var request = CreateRequestMessage ( HttpMethod . Get , url , headers ) ;
191+ Log . Debug ( "[GET FILE] Downloading from {Url}" , url ) ;
192+
193+ var client = Client ;
194+ using var response = await client . SendAsync ( request , cancellationToken ) ;
195+ var bytes = await response . Content . ReadAsByteArrayAsync ( cancellationToken ) ;
196+ var len = bytes . Length ;
197+ Log . Information ( "[GET FILE] {Url} downloaded {Len} bytes" , url , len ) ;
198+ Log . Debug ( "[GET FILE] First 128 bytes (hex) of {Url}: {Preview}" , url , BitConverter . ToString ( bytes , 0 , Math . Min ( 128 , len ) ) ) ;
199+
200+ response . EnsureSuccessStatusCode ( ) ;
201+ return bytes ;
202+ }
203+ catch ( Exception ex )
204+ {
205+ Log . Error ( ex , "GetFileAsync failed for {Url}" , url ) ;
206+ throw ;
207+ }
208+ }
209+
210+ /// <summary>
211+ /// 下载并保存到磁盘
212+ /// </summary>
213+ public async Task SaveFileAsync ( string url , string destPath , Dictionary < string , string > ? headers = null , CancellationToken cancellationToken = default )
214+ {
215+ try
216+ {
217+ var bytes = await GetFileAsync ( url , headers , cancellationToken ) ;
218+
219+ var dir = Path . GetDirectoryName ( destPath ) ;
220+ if ( ! string . IsNullOrEmpty ( dir ) && ! Directory . Exists ( dir ) ) Directory . CreateDirectory ( dir ) ;
221+
222+ await File . WriteAllBytesAsync ( destPath , bytes , cancellationToken ) ;
223+
224+ Log . Information ( "[SAVE FILE] Saved {Url} -> {Path} (len={Len})" , url , destPath , bytes . Length ) ;
225+ Log . Debug ( "[SAVE FILE] Saved file from {Url} to {Path}" , url , destPath ) ;
226+ }
227+ catch ( Exception ex )
228+ {
229+ Log . Error ( ex , "SaveFileAsync failed for {Url} -> {Path}" , url , destPath ) ;
230+ throw ;
231+ }
232+ }
233+
234+ /// <summary>
235+ /// 配置代理:传入 proxyUrl 设置代理,传入 null 则取消代理。
236+ /// 支持用户名/密码和是否使用默认凭据。
237+ /// </summary>
238+ public void ConfigureProxy ( string ? proxyUrl , string ? username = null , string ? password = null , bool bypassOnLocal = false , bool useDefaultCredentials = false )
239+ {
240+ IWebProxy ? proxy = null ;
241+ if ( ! string . IsNullOrWhiteSpace ( proxyUrl ) )
242+ {
243+ var wp = new WebProxy ( proxyUrl ) { BypassProxyOnLocal = bypassOnLocal } ;
244+ if ( ! string . IsNullOrEmpty ( username ) ) wp . Credentials = new NetworkCredential ( username , password ?? string . Empty ) ;
245+ proxy = wp ;
246+ }
247+
248+ ConfigureProxy ( proxy , useDefaultCredentials ) ;
249+ }
250+
251+ /// <summary>
252+ /// 配置代理(IWebProxy 形式)。线程安全,会重建 HttpClient 并替换当前实例。
253+ /// </summary>
254+ public void ConfigureProxy ( IWebProxy ? proxy , bool useDefaultCredentials = false )
255+ {
256+ lock ( _clientLock )
257+ {
258+ Proxy = proxy ;
259+
260+ var old = Client ;
261+ var handler = new HttpClientHandler ( ) ;
262+ if ( proxy != null )
263+ {
264+ handler . Proxy = proxy ;
265+ handler . UseProxy = true ;
266+ handler . UseDefaultCredentials = useDefaultCredentials ;
267+ }
268+ else
269+ {
270+ handler . UseProxy = false ;
271+ }
272+
273+ Client = new HttpClient ( handler ) { Timeout = old . Timeout } ;
274+ try
275+ {
276+ old . Dispose ( ) ;
277+ }
278+ catch
279+ {
280+ // 忽略 dispose 异常
281+ }
282+
283+ Log . Information ( "[PROXY] Configured proxy: {Proxy}, UseDefaultCredentials={UseDefaultCredentials}" , proxy ? . ToString ( ) ?? "none" , useDefaultCredentials ) ;
284+ Log . Debug ( "[PROXY] Proxy object details: {@Proxy}" , proxy ) ;
285+ }
286+ }
287+
288+ /// <summary>
289+ /// 获取当前代理的 ToString() 表示,或 null 表示未配置代理
290+ /// </summary>
291+ public string ? GetCurrentProxy ( ) => Proxy ? . ToString ( ) ;
292+ }
0 commit comments