You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

429 lines
16 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
1 year ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
1 year ago
3 years ago
1 year ago
3 years ago
1 year ago
3 years ago
3 years ago
1 year ago
3 years ago
1 year ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. using Apewer;
  2. using Apewer.Internals;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Net;
  7. using System.Net.Security;
  8. using System.Security;
  9. using System.Security.Cryptography.X509Certificates;
  10. using System.Text;
  11. namespace Apewer.Network
  12. {
  13. /// <summary></summary>
  14. public class HttpClient
  15. {
  16. string _key = TextUtility.Key();
  17. int _block = 1024;
  18. /// <summary></summary>
  19. public string Key { get => _key; }
  20. /// <summary>获取进度的块大小,以字节为单位,默认值:1024(1KB)。</summary>
  21. public int Block { get => _block; set => NumberUtility.Restrict(value, 1, 1048576); }
  22. /// <summary>构建 <see cref="HttpClient"/> 的实例。</summary>
  23. public HttpClient()
  24. {
  25. RequestHeaders = new StringPairs();
  26. }
  27. #region request
  28. /// <summary>获取或设置将要请求的方法,默认为 GET。</summary>
  29. public HttpMethod Method { get; set; }
  30. /// <summary>获取或设置将要请求的地址。</summary>
  31. public string Url { get; set; }
  32. /// <summary>获取或设置请求主体的超时毫秒数,默认值取决于运行时(100,000 毫秒)。</summary>
  33. public int Timeout { get; set; }
  34. /// <summary>获取或设置请求使用的证书。</summary>
  35. public X509Certificate ClientCertificate { get; set; }
  36. /// <summary>获取或设置请求的自定义头。</summary>
  37. public StringPairs RequestHeaders { get; set; }
  38. /// <summary>获取或设置请求的 Cookies。</summary>
  39. public Cookie[] RequestCookies { get; set; }
  40. /// <summary>获取或设置 POST 请求发送的数据。当 Stream 属性有效时此属性将被忽略。</summary>
  41. public byte[] RequestData { get; set; }
  42. /// <summary>获取或设置 POST 请求发送的数据。此属性有效时将忽略 Data 属性。</summary>
  43. public Stream RequestStream { get; set; }
  44. /// <summary>获取或设置写入请求主体的进度回调。</summary>
  45. public Action<long> RequestProgress { get; set; }
  46. /// <summary>获取或设置请求的内容类型,默认为空。此属性可能会被自定义头取代。</summary>
  47. public string RequestContentType { get; set; }
  48. /// <summary>获取或设置请求的内容字节长度,此属性仅在指定大于 0 时被使用。此属性可能会被自定义头或主体字节数组取代。</summary>
  49. public long RequestContentLength { get; set; }
  50. /// <summary>获取或设置用户代理。此属性可能会被自定义头取代。</summary>
  51. public string UserAgent { get; set; }
  52. /// <summary>获取或设置来源。此属性可能会被自定义头取代。</summary>
  53. public string Referer { get; set; }
  54. #endregion
  55. #region response
  56. /// <summary>跟随响应的重定向。</summary>
  57. /// <remarks>默认值:False</remarks>
  58. public bool AllowRedirect { get; set; }
  59. /// <summary>获取或设置要接收响应主体的流。默认为 NULL 值。</summary>
  60. /// <remarks>指定为 NULL 时,响应体将写入字节数组;<br />非 NULL 时,响应体将写入此流,并忽略 ResponseData 属性。</remarks>
  61. public Stream ResponseStream { get; set; }
  62. /// <summary>获取或设置读取响应主体的进度回调</summary>
  63. public Action<long> ResponseProgress { get; set; }
  64. /// <summary>获取响应的状态。</summary>
  65. public string ResponseStatus { get; private set; }
  66. /// <summary>获取响应的缓存状态。</summary>
  67. public bool ResponseCached { get; private set; }
  68. /// <summary>获取响应的头。</summary>
  69. public Cookie[] ResponseCookies { get; private set; }
  70. /// <summary>获取响应的头。</summary>
  71. public StringPairs ResponseHeaders { get; private set; }
  72. /// <summary>获取响应的主体,当指定流时此属性将不被使用。</summary>
  73. public byte[] ResponseData { get; private set; }
  74. #endregion
  75. #region send
  76. object _locker = new object();
  77. Exception _exception = null;
  78. HttpWebRequest _request = null;
  79. HttpWebResponse _response = null;
  80. Nullable<DateTime> _request_time = null;
  81. Nullable<DateTime> _response_time = null;
  82. /// <summary>发送请求的时间。</summary>
  83. public Nullable<DateTime> RequestTime { get => _request_time; }
  84. /// <summary>接收响应的时间。</summary>
  85. public Nullable<DateTime> ResponseTime { get => _response_time; }
  86. /// <summary>获取最近发生的异常。</summary>
  87. public Exception Exception { get { return _exception; } }
  88. Exception Return(Exception ex)
  89. {
  90. _exception = ex;
  91. return ex;
  92. }
  93. /// <summary>发送请求,并获取响应。</summary>
  94. /// <param name="catchEx">捕获发生的异常,将异常作为返回值。</param>
  95. /// <returns>发生的异常。</returns>
  96. public Exception Send(bool catchEx = true)
  97. {
  98. lock (_locker)
  99. {
  100. _exception = null;
  101. ResponseStatus = default;
  102. ResponseCached = default;
  103. ResponseCookies = default;
  104. ResponseHeaders = default;
  105. ResponseData = default;
  106. var url = Url;
  107. if (url.IsEmpty())
  108. {
  109. var ex = new MissingMemberException("未指定 URL。");
  110. if (!catchEx) throw ex;
  111. return Return(ex);
  112. }
  113. var method = Method;
  114. if (method == HttpMethod.NULL)
  115. {
  116. if (RequestData != null || RequestStream != null) method = HttpMethod.POST;
  117. else method = HttpMethod.GET;
  118. }
  119. if (catchEx)
  120. {
  121. try
  122. {
  123. _request = Prepare(url, method);
  124. _request_time = DateTime.Now;
  125. _response = _request.GetResponse() as HttpWebResponse;
  126. _response_time = DateTime.Now;
  127. Parse(_response);
  128. }
  129. catch (Exception ex)
  130. {
  131. _response_time = DateTime.Now;
  132. _exception = ex;
  133. if (ex is WebException webEx) Parse((HttpWebResponse)webEx.Response);
  134. return ex;
  135. }
  136. }
  137. else
  138. {
  139. _request = Prepare(url, method);
  140. _request_time = DateTime.Now;
  141. _response = _request.GetResponse() as HttpWebResponse;
  142. _response_time = DateTime.Now;
  143. Parse(_response);
  144. }
  145. }
  146. return null;
  147. }
  148. /// <exception cref="System.ArgumentException"></exception>
  149. /// <exception cref="System.ArgumentNullException"></exception>
  150. /// <exception cref="System.InvalidOperationException"></exception>
  151. /// <exception cref="System.NotSupportedException"></exception>
  152. /// <exception cref="System.ObjectDisposedException"></exception>
  153. /// <exception cref="System.Security.SecurityException"></exception>
  154. /// <exception cref="System.Net.ProtocolViolationException"></exception>
  155. /// <exception cref="System.Net.WebException"></exception>
  156. HttpWebRequest Prepare(string url, HttpMethod method)
  157. {
  158. var https = url.ToLower().StartsWith("https");
  159. if (https) SslUtility.ApproveValidation();
  160. var request = (HttpWebRequest)WebRequest.Create(url);
  161. var certificate = ClientCertificate;
  162. if (certificate != null) request.ClientCertificates.Add(certificate);
  163. var timeout = Timeout;
  164. if (timeout > 0) request.Timeout = timeout;
  165. request.Method = method.ToString();
  166. request.AllowAutoRedirect = AllowRedirect;
  167. var cookies = RequestCookies;
  168. if (cookies != null)
  169. {
  170. foreach (var cookie in cookies)
  171. {
  172. if (cookie == null) continue;
  173. request.CookieContainer.Add(cookie);
  174. }
  175. }
  176. var length = RequestContentLength;
  177. var type = RequestContentType;
  178. var ua = UserAgent;
  179. var r = Referer;
  180. var headers = RequestHeaders;
  181. if (headers != null) foreach (var header in headers)
  182. {
  183. var key = header.Key.ToTrim(); ;
  184. var value = header.Value.ToTrim();
  185. if (TextUtility.IsBlank(key)) continue;
  186. if (TextUtility.IsBlank(value)) continue;
  187. try
  188. {
  189. switch (key.Lower())
  190. {
  191. case "accept":
  192. request.Accept = value;
  193. break;
  194. case "connection":
  195. request.Connection = value;
  196. break;
  197. case "content-length":
  198. case "contentlength":
  199. length = Math.Max(length, NumberUtility.Int64(value));
  200. break;
  201. case "content-type":
  202. case "contenttype":
  203. type = value;
  204. break;
  205. case "expect":
  206. request.Expect = value;
  207. break;
  208. #if !NET20
  209. case "host":
  210. request.Host = value;
  211. break;
  212. #endif
  213. case "if-modified-since":
  214. var requestIfModifiedSince = ClockUtility.Parse(value);
  215. if (requestIfModifiedSince != null) request.IfModifiedSince = requestIfModifiedSince.Value;
  216. break;
  217. case "referer":
  218. case "referrer":
  219. r = value;
  220. break;
  221. case "transfer-encoding":
  222. request.TransferEncoding = value;
  223. break;
  224. case "user-agent":
  225. case "useragent":
  226. ua = value;
  227. break;
  228. default:
  229. request.Headers.Add(header.Key, header.Value);
  230. break;
  231. }
  232. }
  233. catch { }
  234. }
  235. if (length > 0L) request.ContentLength = length;
  236. if (type.NotEmpty()) request.ContentType = type;
  237. if (ua.NotEmpty()) request.UserAgent = ua;
  238. if (r.NotEmpty()) request.Referer = r;
  239. if (method == HttpMethod.POST)
  240. {
  241. var stream = RequestStream;
  242. var data = RequestData;
  243. if (stream != null)
  244. {
  245. var body = request.GetRequestStream();
  246. BytesUtility.Read(stream, body, RequestProgress, Block);
  247. }
  248. else if (data != null)
  249. {
  250. request.ContentLength = data.LongLength;
  251. var body = request.GetRequestStream();
  252. BytesUtility.Write(body, data, RequestProgress, Block);
  253. }
  254. }
  255. return request;
  256. }
  257. /// <exception cref="System.ArgumentNullException"></exception>
  258. /// <exception cref="System.InvalidOperationException"></exception>
  259. /// <exception cref="System.NotSupportedException"></exception>
  260. /// <exception cref="System.ObjectDisposedException"></exception>
  261. /// <exception cref="System.Net.ProtocolViolationException"></exception>
  262. /// <exception cref="System.Net.WebException"></exception>
  263. void Parse(HttpWebResponse response)
  264. {
  265. if (response == null) return;
  266. ResponseStatus = ((int)response.StatusCode).ToString();
  267. ResponseCached = response.IsFromCache;
  268. var headers = new StringPairs();
  269. foreach (var key in response.Headers.AllKeys)
  270. {
  271. var values = response.Headers.GetValues(key);
  272. headers.Add(key, values.Join(","));
  273. }
  274. ResponseHeaders = headers;
  275. var cb = new ArrayBuilder<Cookie>();
  276. foreach (var item in response.Cookies)
  277. {
  278. var cookie = item as Cookie;
  279. if (cookie == null) continue;
  280. cb.Add(cookie);
  281. }
  282. var body = response.GetResponseStream();
  283. var progress = ResponseProgress;
  284. var hasProgress = progress != null;
  285. var stream = ResponseStream;
  286. if (stream == null) ResponseData = BytesUtility.Read(body);
  287. else BytesUtility.Read(body, stream, progress, Block);
  288. response.Close();
  289. }
  290. #endregion
  291. #region static
  292. const int SimpleTimeout = 30000;
  293. /// <summary>GET</summary>
  294. public static HttpClient Get(string url, int timeout = 30000)
  295. {
  296. var http = new HttpClient();
  297. http.Url = url;
  298. http.Timeout = timeout;
  299. http.Method = HttpMethod.GET;
  300. http.Send();
  301. return http;
  302. }
  303. /// <summary>POST</summary>
  304. public static HttpClient Post(string url, byte[] data, int timeout = 30000, string type = "application/octet-stream")
  305. {
  306. var http = new HttpClient();
  307. http.Url = url;
  308. http.Timeout = timeout;
  309. http.Method = HttpMethod.POST;
  310. if (!string.IsNullOrEmpty(type)) http.RequestContentType = type;
  311. if (data != null)
  312. {
  313. http.RequestContentLength = data.LongLength;
  314. http.RequestData = data;
  315. }
  316. http.Send();
  317. return http;
  318. }
  319. /// <summary>POST text/plain</summary>
  320. public static HttpClient Text(string url, string text, int timeout = 30000, string type = "text/plain")
  321. {
  322. return Post(url, TextUtility.Bytes(text), timeout, type);
  323. }
  324. /// <summary>POST application/x-www-form-urlencoded</summary>
  325. public static HttpClient Form(string url, IDictionary<string, string> form, int timeout = 30000)
  326. {
  327. if (form == null) return Post(url, BytesUtility.Empty, timeout, "application/x-www-form-urlencoded");
  328. var cache = new List<string>();
  329. foreach (var i in form)
  330. {
  331. var key = TextUtility.EncodeUrl(i.Key);
  332. var value = TextUtility.EncodeUrl(i.Value);
  333. cache.Add(key + "=" + value);
  334. }
  335. var text = string.Join("&", cache.ToArray());
  336. var data = TextUtility.Bytes(text);
  337. return Post(url, data, timeout, "application/x-www-form-urlencoded");
  338. }
  339. /// <summary>POST application/x-www-form-urlencoded</summary>
  340. public static HttpClient Form(string url, Dictionary<string, string> form, int timeout = 30000) => Form(url, form as IDictionary<string, string>, timeout);
  341. /// <summary>合并表单参数,不包含 Query 的 ? 符号。</summary>
  342. public static string MergeForm(Dictionary<string, string> form)
  343. {
  344. if (form == null) return "";
  345. if (form.Count < 1) return "";
  346. var cache = new List<string>();
  347. foreach (var i in form)
  348. {
  349. var key = TextUtility.EncodeUrl(i.Key);
  350. var value = TextUtility.EncodeUrl(i.Value);
  351. cache.Add(key + "=" + value);
  352. }
  353. var text = string.Join("&", cache.ToArray());
  354. return text;
  355. }
  356. #endregion
  357. }
  358. }