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.

415 lines
16 KiB

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
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. /// <summary>获取最近发生的异常。</summary>
  81. public Exception Exception { get { return _exception; } }
  82. Exception Return(Exception ex)
  83. {
  84. _exception = ex;
  85. return ex;
  86. }
  87. /// <summary>发送请求,并获取响应。</summary>
  88. /// <param name="catchEx">捕获发生的异常,将异常作为返回值。</param>
  89. /// <returns>发生的异常。</returns>
  90. public Exception Send(bool catchEx = true)
  91. {
  92. lock (_locker)
  93. {
  94. _exception = null;
  95. ResponseStatus = default;
  96. ResponseCached = default;
  97. ResponseCookies = default;
  98. ResponseHeaders = default;
  99. ResponseData = default;
  100. var url = Url;
  101. if (url.IsEmpty())
  102. {
  103. var ex = new MissingMemberException("未指定 URL。");
  104. if (!catchEx) throw ex;
  105. return Return(ex);
  106. }
  107. var method = Method;
  108. if (method == HttpMethod.NULL)
  109. {
  110. if (RequestData != null || RequestStream != null) method = HttpMethod.POST;
  111. else method = HttpMethod.GET;
  112. }
  113. if (catchEx)
  114. {
  115. try
  116. {
  117. _request = Prepare(url, method);
  118. _response = _request.GetResponse() as HttpWebResponse;
  119. Parse(_response);
  120. }
  121. catch (Exception ex)
  122. {
  123. _exception = ex;
  124. if (ex is WebException webEx) Parse((HttpWebResponse)webEx.Response);
  125. return ex;
  126. }
  127. }
  128. else
  129. {
  130. _request = Prepare(url, method);
  131. _response = _request.GetResponse() as HttpWebResponse;
  132. Parse(_response);
  133. }
  134. }
  135. return null;
  136. }
  137. /// <exception cref="System.ArgumentException"></exception>
  138. /// <exception cref="System.ArgumentNullException"></exception>
  139. /// <exception cref="System.InvalidOperationException"></exception>
  140. /// <exception cref="System.NotSupportedException"></exception>
  141. /// <exception cref="System.ObjectDisposedException"></exception>
  142. /// <exception cref="System.Security.SecurityException"></exception>
  143. /// <exception cref="System.Net.ProtocolViolationException"></exception>
  144. /// <exception cref="System.Net.WebException"></exception>
  145. HttpWebRequest Prepare(string url, HttpMethod method)
  146. {
  147. var https = url.ToLower().StartsWith("https");
  148. if (https) SslUtility.ApproveValidation();
  149. var request = (HttpWebRequest)WebRequest.Create(url);
  150. var certificate = ClientCertificate;
  151. if (certificate != null) request.ClientCertificates.Add(certificate);
  152. var timeout = Timeout;
  153. if (timeout > 0) request.Timeout = timeout;
  154. request.Method = method.ToString();
  155. request.AllowAutoRedirect = AllowRedirect;
  156. var cookies = RequestCookies;
  157. if (cookies != null)
  158. {
  159. foreach (var cookie in cookies)
  160. {
  161. if (cookie == null) continue;
  162. request.CookieContainer.Add(cookie);
  163. }
  164. }
  165. var length = RequestContentLength;
  166. var type = RequestContentType;
  167. var ua = UserAgent;
  168. var r = Referer;
  169. var headers = RequestHeaders;
  170. if (headers != null) foreach (var header in headers)
  171. {
  172. var key = header.Key.ToTrim(); ;
  173. var value = header.Value.ToTrim();
  174. if (TextUtility.IsBlank(key)) continue;
  175. if (TextUtility.IsBlank(value)) continue;
  176. try
  177. {
  178. switch (key.Lower())
  179. {
  180. case "accept":
  181. request.Accept = value;
  182. break;
  183. case "connection":
  184. request.Connection = value;
  185. break;
  186. case "content-length":
  187. case "contentlength":
  188. length = Math.Max(length, NumberUtility.Int64(value));
  189. break;
  190. case "content-type":
  191. case "contenttype":
  192. type = value;
  193. break;
  194. case "expect":
  195. request.Expect = value;
  196. break;
  197. #if !NET20
  198. case "host":
  199. request.Host = value;
  200. break;
  201. #endif
  202. case "if-modified-since":
  203. var requestIfModifiedSince = ClockUtility.Parse(value);
  204. if (requestIfModifiedSince != null) request.IfModifiedSince = requestIfModifiedSince.Value;
  205. break;
  206. case "referer":
  207. case "referrer":
  208. r = value;
  209. break;
  210. case "transfer-encoding":
  211. request.TransferEncoding = value;
  212. break;
  213. case "user-agent":
  214. case "useragent":
  215. ua = value;
  216. break;
  217. default:
  218. request.Headers.Add(header.Key, header.Value);
  219. break;
  220. }
  221. }
  222. catch { }
  223. }
  224. if (length > 0L) request.ContentLength = length;
  225. if (type.NotEmpty()) request.ContentType = type;
  226. if (ua.NotEmpty()) request.UserAgent = ua;
  227. if (r.NotEmpty()) request.Referer = r;
  228. if (method == HttpMethod.POST)
  229. {
  230. var stream = RequestStream;
  231. var data = RequestData;
  232. if (stream != null)
  233. {
  234. var body = request.GetRequestStream();
  235. BytesUtility.Read(stream, body, RequestProgress, Block);
  236. }
  237. else if (data != null)
  238. {
  239. request.ContentLength = data.LongLength;
  240. var body = request.GetRequestStream();
  241. BytesUtility.Write(body, data, RequestProgress, Block);
  242. }
  243. }
  244. return request;
  245. }
  246. /// <exception cref="System.ArgumentNullException"></exception>
  247. /// <exception cref="System.InvalidOperationException"></exception>
  248. /// <exception cref="System.NotSupportedException"></exception>
  249. /// <exception cref="System.ObjectDisposedException"></exception>
  250. /// <exception cref="System.Net.ProtocolViolationException"></exception>
  251. /// <exception cref="System.Net.WebException"></exception>
  252. void Parse(HttpWebResponse response)
  253. {
  254. if (response == null) return;
  255. ResponseStatus = ((int)response.StatusCode).ToString();
  256. ResponseCached = response.IsFromCache;
  257. var headers = new StringPairs();
  258. foreach (var key in response.Headers.AllKeys)
  259. {
  260. var values = response.Headers.GetValues(key);
  261. headers.Add(key, values.Join(","));
  262. }
  263. ResponseHeaders = headers;
  264. var cb = new ArrayBuilder<Cookie>();
  265. foreach (var item in response.Cookies)
  266. {
  267. var cookie = item as Cookie;
  268. if (cookie == null) continue;
  269. cb.Add(cookie);
  270. }
  271. var body = response.GetResponseStream();
  272. var progress = ResponseProgress;
  273. var hasProgress = progress != null;
  274. var stream = ResponseStream;
  275. if (stream == null) ResponseData = BytesUtility.Read(body);
  276. else BytesUtility.Read(body, stream, progress, Block);
  277. response.Close();
  278. }
  279. #endregion
  280. #region static
  281. const int SimpleTimeout = 30000;
  282. /// <summary>GET</summary>
  283. public static HttpClient Get(string url, int timeout = 30000)
  284. {
  285. var http = new HttpClient();
  286. http.Url = url;
  287. http.Timeout = timeout;
  288. http.Method = HttpMethod.GET;
  289. http.Send();
  290. return http;
  291. }
  292. /// <summary>POST</summary>
  293. public static HttpClient Post(string url, byte[] data, int timeout = 30000, string type = "application/octet-stream")
  294. {
  295. var http = new HttpClient();
  296. http.Url = url;
  297. http.Timeout = timeout;
  298. http.Method = HttpMethod.POST;
  299. if (!string.IsNullOrEmpty(type)) http.RequestContentType = type;
  300. if (data != null)
  301. {
  302. http.RequestContentLength = data.LongLength;
  303. http.RequestData = data;
  304. }
  305. http.Send();
  306. return http;
  307. }
  308. /// <summary>POST text/plain</summary>
  309. public static HttpClient Text(string url, string text, int timeout = 30000, string type = "text/plain")
  310. {
  311. return Post(url, TextUtility.Bytes(text), timeout, type);
  312. }
  313. /// <summary>POST application/x-www-form-urlencoded</summary>
  314. public static HttpClient Form(string url, IDictionary<string, string> form, int timeout = 30000)
  315. {
  316. if (form == null) return Post(url, BytesUtility.Empty, timeout, "application/x-www-form-urlencoded");
  317. var cache = new List<string>();
  318. foreach (var i in form)
  319. {
  320. var key = TextUtility.EncodeUrl(i.Key);
  321. var value = TextUtility.EncodeUrl(i.Value);
  322. cache.Add(key + "=" + value);
  323. }
  324. var text = string.Join("&", cache.ToArray());
  325. var data = TextUtility.Bytes(text);
  326. return Post(url, data, timeout, "application/x-www-form-urlencoded");
  327. }
  328. /// <summary>POST application/x-www-form-urlencoded</summary>
  329. public static HttpClient Form(string url, Dictionary<string, string> form, int timeout = 30000) => Form(url, form as IDictionary<string, string>, timeout);
  330. /// <summary>合并表单参数,不包含 Query 的 ? 符号。</summary>
  331. public static string MergeForm(Dictionary<string, string> form)
  332. {
  333. if (form == null) return "";
  334. if (form.Count < 1) return "";
  335. var cache = new List<string>();
  336. foreach (var i in form)
  337. {
  338. var key = TextUtility.EncodeUrl(i.Key);
  339. var value = TextUtility.EncodeUrl(i.Value);
  340. cache.Add(key + "=" + value);
  341. }
  342. var text = string.Join("&", cache.ToArray());
  343. return text;
  344. }
  345. #endregion
  346. }
  347. }