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.

430 lines
16 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
9 months ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
9 months ago
3 years ago
9 months ago
3 years ago
9 months ago
3 years ago
3 years ago
9 months ago
3 years ago
9 months 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. request.CookieContainer = new CookieContainer();
  168. var cookies = RequestCookies;
  169. if (cookies != null)
  170. {
  171. foreach (var cookie in cookies)
  172. {
  173. if (cookie == null) continue;
  174. request.CookieContainer.Add(cookie);
  175. }
  176. }
  177. var length = RequestContentLength;
  178. var type = RequestContentType;
  179. var ua = UserAgent;
  180. var r = Referer;
  181. var headers = RequestHeaders;
  182. if (headers != null) foreach (var header in headers)
  183. {
  184. var key = header.Key.ToTrim(); ;
  185. var value = header.Value.ToTrim();
  186. if (TextUtility.IsBlank(key)) continue;
  187. if (TextUtility.IsBlank(value)) continue;
  188. try
  189. {
  190. switch (key.Lower())
  191. {
  192. case "accept":
  193. request.Accept = value;
  194. break;
  195. case "connection":
  196. request.Connection = value;
  197. break;
  198. case "content-length":
  199. case "contentlength":
  200. length = Math.Max(length, NumberUtility.Int64(value));
  201. break;
  202. case "content-type":
  203. case "contenttype":
  204. type = value;
  205. break;
  206. case "expect":
  207. request.Expect = value;
  208. break;
  209. #if !NET20
  210. case "host":
  211. request.Host = value;
  212. break;
  213. #endif
  214. case "if-modified-since":
  215. var requestIfModifiedSince = ClockUtility.Parse(value);
  216. if (requestIfModifiedSince != null) request.IfModifiedSince = requestIfModifiedSince.Value;
  217. break;
  218. case "referer":
  219. case "referrer":
  220. r = value;
  221. break;
  222. case "transfer-encoding":
  223. request.TransferEncoding = value;
  224. break;
  225. case "user-agent":
  226. case "useragent":
  227. ua = value;
  228. break;
  229. default:
  230. request.Headers.Add(header.Key, header.Value);
  231. break;
  232. }
  233. }
  234. catch { }
  235. }
  236. if (length > 0L) request.ContentLength = length;
  237. if (type.NotEmpty()) request.ContentType = type;
  238. if (ua.NotEmpty()) request.UserAgent = ua;
  239. if (r.NotEmpty()) request.Referer = r;
  240. if (method == HttpMethod.POST)
  241. {
  242. var stream = RequestStream;
  243. var data = RequestData;
  244. if (stream != null)
  245. {
  246. var body = request.GetRequestStream();
  247. BytesUtility.Read(stream, body, RequestProgress, Block);
  248. }
  249. else if (data != null)
  250. {
  251. request.ContentLength = data.LongLength;
  252. var body = request.GetRequestStream();
  253. BytesUtility.Write(body, data, RequestProgress, Block);
  254. }
  255. }
  256. return request;
  257. }
  258. /// <exception cref="System.ArgumentNullException"></exception>
  259. /// <exception cref="System.InvalidOperationException"></exception>
  260. /// <exception cref="System.NotSupportedException"></exception>
  261. /// <exception cref="System.ObjectDisposedException"></exception>
  262. /// <exception cref="System.Net.ProtocolViolationException"></exception>
  263. /// <exception cref="System.Net.WebException"></exception>
  264. void Parse(HttpWebResponse response)
  265. {
  266. if (response == null) return;
  267. ResponseStatus = ((int)response.StatusCode).ToString();
  268. ResponseCached = response.IsFromCache;
  269. var headers = new StringPairs();
  270. foreach (var key in response.Headers.AllKeys)
  271. {
  272. var values = response.Headers.GetValues(key);
  273. headers.Add(key, values.Join(","));
  274. }
  275. ResponseHeaders = headers;
  276. var cb = new ArrayBuilder<Cookie>();
  277. foreach (var item in response.Cookies)
  278. {
  279. var cookie = item as Cookie;
  280. if (cookie == null) continue;
  281. cb.Add(cookie);
  282. }
  283. var body = response.GetResponseStream();
  284. var progress = ResponseProgress;
  285. var hasProgress = progress != null;
  286. var stream = ResponseStream;
  287. if (stream == null) ResponseData = BytesUtility.Read(body);
  288. else BytesUtility.Read(body, stream, progress, Block);
  289. response.Close();
  290. }
  291. #endregion
  292. #region static
  293. const int SimpleTimeout = 30000;
  294. /// <summary>GET</summary>
  295. public static HttpClient Get(string url, int timeout = 30000)
  296. {
  297. var http = new HttpClient();
  298. http.Url = url;
  299. http.Timeout = timeout;
  300. http.Method = HttpMethod.GET;
  301. http.Send();
  302. return http;
  303. }
  304. /// <summary>POST</summary>
  305. public static HttpClient Post(string url, byte[] data, int timeout = 30000, string type = "application/octet-stream")
  306. {
  307. var http = new HttpClient();
  308. http.Url = url;
  309. http.Timeout = timeout;
  310. http.Method = HttpMethod.POST;
  311. if (!string.IsNullOrEmpty(type)) http.RequestContentType = type;
  312. if (data != null)
  313. {
  314. http.RequestContentLength = data.LongLength;
  315. http.RequestData = data;
  316. }
  317. http.Send();
  318. return http;
  319. }
  320. /// <summary>POST text/plain</summary>
  321. public static HttpClient Text(string url, string text, int timeout = 30000, string type = "text/plain")
  322. {
  323. return Post(url, TextUtility.Bytes(text), timeout, type);
  324. }
  325. /// <summary>POST application/x-www-form-urlencoded</summary>
  326. public static HttpClient Form(string url, IDictionary<string, string> form, int timeout = 30000)
  327. {
  328. if (form == null) return Post(url, BytesUtility.Empty, timeout, "application/x-www-form-urlencoded");
  329. var cache = new List<string>();
  330. foreach (var i in form)
  331. {
  332. var key = TextUtility.EncodeUrl(i.Key);
  333. var value = TextUtility.EncodeUrl(i.Value);
  334. cache.Add(key + "=" + value);
  335. }
  336. var text = string.Join("&", cache.ToArray());
  337. var data = TextUtility.Bytes(text);
  338. return Post(url, data, timeout, "application/x-www-form-urlencoded");
  339. }
  340. /// <summary>POST application/x-www-form-urlencoded</summary>
  341. public static HttpClient Form(string url, Dictionary<string, string> form, int timeout = 30000) => Form(url, form as IDictionary<string, string>, timeout);
  342. /// <summary>合并表单参数,不包含 Query 的 ? 符号。</summary>
  343. public static string MergeForm(Dictionary<string, string> form)
  344. {
  345. if (form == null) return "";
  346. if (form.Count < 1) return "";
  347. var cache = new List<string>();
  348. foreach (var i in form)
  349. {
  350. var key = TextUtility.EncodeUrl(i.Key);
  351. var value = TextUtility.EncodeUrl(i.Value);
  352. cache.Add(key + "=" + value);
  353. }
  354. var text = string.Join("&", cache.ToArray());
  355. return text;
  356. }
  357. #endregion
  358. }
  359. }