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
430 lines
16 KiB
using Apewer;
|
|
using Apewer.Internals;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Security;
|
|
using System.Security;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
|
|
namespace Apewer.Network
|
|
{
|
|
|
|
/// <summary></summary>
|
|
public class HttpClient
|
|
{
|
|
|
|
string _key = TextUtility.Key();
|
|
int _block = 1024;
|
|
|
|
/// <summary></summary>
|
|
public string Key { get => _key; }
|
|
|
|
/// <summary>获取进度的块大小,以字节为单位,默认值:1024(1KB)。</summary>
|
|
public int Block { get => _block; set => NumberUtility.Restrict(value, 1, 1048576); }
|
|
|
|
/// <summary>构建 <see cref="HttpClient"/> 的实例。</summary>
|
|
public HttpClient()
|
|
{
|
|
RequestHeaders = new StringPairs();
|
|
}
|
|
|
|
#region request
|
|
|
|
/// <summary>获取或设置将要请求的方法,默认为 GET。</summary>
|
|
public HttpMethod Method { get; set; }
|
|
|
|
/// <summary>获取或设置将要请求的地址。</summary>
|
|
public string Url { get; set; }
|
|
|
|
/// <summary>获取或设置请求主体的超时毫秒数,默认值取决于运行时(100,000 毫秒)。</summary>
|
|
public int Timeout { get; set; }
|
|
|
|
/// <summary>获取或设置请求使用的证书。</summary>
|
|
public X509Certificate ClientCertificate { get; set; }
|
|
|
|
/// <summary>获取或设置请求的自定义头。</summary>
|
|
public StringPairs RequestHeaders { get; set; }
|
|
|
|
/// <summary>获取或设置请求的 Cookies。</summary>
|
|
public Cookie[] RequestCookies { get; set; }
|
|
|
|
/// <summary>获取或设置 POST 请求发送的数据。当 Stream 属性有效时此属性将被忽略。</summary>
|
|
public byte[] RequestData { get; set; }
|
|
|
|
/// <summary>获取或设置 POST 请求发送的数据。此属性有效时将忽略 Data 属性。</summary>
|
|
public Stream RequestStream { get; set; }
|
|
|
|
/// <summary>获取或设置写入请求主体的进度回调。</summary>
|
|
public Action<long> RequestProgress { get; set; }
|
|
|
|
/// <summary>获取或设置请求的内容类型,默认为空。此属性可能会被自定义头取代。</summary>
|
|
public string RequestContentType { get; set; }
|
|
|
|
/// <summary>获取或设置请求的内容字节长度,此属性仅在指定大于 0 时被使用。此属性可能会被自定义头或主体字节数组取代。</summary>
|
|
public long RequestContentLength { get; set; }
|
|
|
|
/// <summary>获取或设置用户代理。此属性可能会被自定义头取代。</summary>
|
|
public string UserAgent { get; set; }
|
|
|
|
/// <summary>获取或设置来源。此属性可能会被自定义头取代。</summary>
|
|
public string Referer { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region response
|
|
|
|
/// <summary>跟随响应的重定向。</summary>
|
|
/// <remarks>默认值:False</remarks>
|
|
public bool AllowRedirect { get; set; }
|
|
|
|
/// <summary>获取或设置要接收响应主体的流。默认为 NULL 值。</summary>
|
|
/// <remarks>指定为 NULL 时,响应体将写入字节数组;<br />非 NULL 时,响应体将写入此流,并忽略 ResponseData 属性。</remarks>
|
|
public Stream ResponseStream { get; set; }
|
|
|
|
/// <summary>获取或设置读取响应主体的进度回调</summary>
|
|
public Action<long> ResponseProgress { get; set; }
|
|
|
|
/// <summary>获取响应的状态。</summary>
|
|
public string ResponseStatus { get; private set; }
|
|
|
|
/// <summary>获取响应的缓存状态。</summary>
|
|
public bool ResponseCached { get; private set; }
|
|
|
|
/// <summary>获取响应的头。</summary>
|
|
public Cookie[] ResponseCookies { get; private set; }
|
|
|
|
/// <summary>获取响应的头。</summary>
|
|
public StringPairs ResponseHeaders { get; private set; }
|
|
|
|
/// <summary>获取响应的主体,当指定流时此属性将不被使用。</summary>
|
|
public byte[] ResponseData { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region send
|
|
|
|
object _locker = new object();
|
|
Exception _exception = null;
|
|
HttpWebRequest _request = null;
|
|
HttpWebResponse _response = null;
|
|
|
|
Nullable<DateTime> _request_time = null;
|
|
Nullable<DateTime> _response_time = null;
|
|
|
|
/// <summary>发送请求的时间。</summary>
|
|
public Nullable<DateTime> RequestTime { get => _request_time; }
|
|
|
|
/// <summary>接收响应的时间。</summary>
|
|
public Nullable<DateTime> ResponseTime { get => _response_time; }
|
|
|
|
/// <summary>获取最近发生的异常。</summary>
|
|
public Exception Exception { get { return _exception; } }
|
|
|
|
Exception Return(Exception ex)
|
|
{
|
|
_exception = ex;
|
|
return ex;
|
|
}
|
|
|
|
/// <summary>发送请求,并获取响应。</summary>
|
|
/// <param name="catchEx">捕获发生的异常,将异常作为返回值。</param>
|
|
/// <returns>发生的异常。</returns>
|
|
public Exception Send(bool catchEx = true)
|
|
{
|
|
lock (_locker)
|
|
{
|
|
_exception = null;
|
|
|
|
ResponseStatus = default;
|
|
ResponseCached = default;
|
|
ResponseCookies = default;
|
|
ResponseHeaders = default;
|
|
ResponseData = default;
|
|
|
|
var url = Url;
|
|
if (url.IsEmpty())
|
|
{
|
|
var ex = new MissingMemberException("未指定 URL。");
|
|
if (!catchEx) throw ex;
|
|
return Return(ex);
|
|
}
|
|
|
|
var method = Method;
|
|
if (method == HttpMethod.NULL)
|
|
{
|
|
if (RequestData != null || RequestStream != null) method = HttpMethod.POST;
|
|
else method = HttpMethod.GET;
|
|
}
|
|
|
|
if (catchEx)
|
|
{
|
|
try
|
|
{
|
|
_request = Prepare(url, method);
|
|
_request_time = DateTime.Now;
|
|
_response = _request.GetResponse() as HttpWebResponse;
|
|
_response_time = DateTime.Now;
|
|
Parse(_response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_response_time = DateTime.Now;
|
|
_exception = ex;
|
|
if (ex is WebException webEx) Parse((HttpWebResponse)webEx.Response);
|
|
return ex;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_request = Prepare(url, method);
|
|
_request_time = DateTime.Now;
|
|
_response = _request.GetResponse() as HttpWebResponse;
|
|
_response_time = DateTime.Now;
|
|
Parse(_response);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <exception cref="System.ArgumentException"></exception>
|
|
/// <exception cref="System.ArgumentNullException"></exception>
|
|
/// <exception cref="System.InvalidOperationException"></exception>
|
|
/// <exception cref="System.NotSupportedException"></exception>
|
|
/// <exception cref="System.ObjectDisposedException"></exception>
|
|
/// <exception cref="System.Security.SecurityException"></exception>
|
|
/// <exception cref="System.Net.ProtocolViolationException"></exception>
|
|
/// <exception cref="System.Net.WebException"></exception>
|
|
HttpWebRequest Prepare(string url, HttpMethod method)
|
|
{
|
|
var https = url.ToLower().StartsWith("https");
|
|
if (https) SslUtility.ApproveValidation();
|
|
|
|
var request = (HttpWebRequest)WebRequest.Create(url);
|
|
|
|
var certificate = ClientCertificate;
|
|
if (certificate != null) request.ClientCertificates.Add(certificate);
|
|
|
|
var timeout = Timeout;
|
|
if (timeout > 0) request.Timeout = timeout;
|
|
request.Method = method.ToString();
|
|
request.AllowAutoRedirect = AllowRedirect;
|
|
|
|
request.CookieContainer = new CookieContainer();
|
|
var cookies = RequestCookies;
|
|
if (cookies != null)
|
|
{
|
|
foreach (var cookie in cookies)
|
|
{
|
|
if (cookie == null) continue;
|
|
request.CookieContainer.Add(cookie);
|
|
}
|
|
}
|
|
|
|
var length = RequestContentLength;
|
|
var type = RequestContentType;
|
|
var ua = UserAgent;
|
|
var r = Referer;
|
|
var headers = RequestHeaders;
|
|
if (headers != null) foreach (var header in headers)
|
|
{
|
|
var key = header.Key.ToTrim(); ;
|
|
var value = header.Value.ToTrim();
|
|
if (TextUtility.IsBlank(key)) continue;
|
|
if (TextUtility.IsBlank(value)) continue;
|
|
try
|
|
{
|
|
switch (key.Lower())
|
|
{
|
|
case "accept":
|
|
request.Accept = value;
|
|
break;
|
|
case "connection":
|
|
request.Connection = value;
|
|
break;
|
|
case "content-length":
|
|
case "contentlength":
|
|
length = Math.Max(length, NumberUtility.Int64(value));
|
|
break;
|
|
case "content-type":
|
|
case "contenttype":
|
|
type = value;
|
|
break;
|
|
case "expect":
|
|
request.Expect = value;
|
|
break;
|
|
#if !NET20
|
|
case "host":
|
|
request.Host = value;
|
|
break;
|
|
#endif
|
|
case "if-modified-since":
|
|
var requestIfModifiedSince = ClockUtility.Parse(value);
|
|
if (requestIfModifiedSince != null) request.IfModifiedSince = requestIfModifiedSince.Value;
|
|
break;
|
|
case "referer":
|
|
case "referrer":
|
|
r = value;
|
|
break;
|
|
case "transfer-encoding":
|
|
request.TransferEncoding = value;
|
|
break;
|
|
case "user-agent":
|
|
case "useragent":
|
|
ua = value;
|
|
break;
|
|
default:
|
|
request.Headers.Add(header.Key, header.Value);
|
|
break;
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
if (length > 0L) request.ContentLength = length;
|
|
if (type.NotEmpty()) request.ContentType = type;
|
|
if (ua.NotEmpty()) request.UserAgent = ua;
|
|
if (r.NotEmpty()) request.Referer = r;
|
|
|
|
if (method == HttpMethod.POST)
|
|
{
|
|
var stream = RequestStream;
|
|
var data = RequestData;
|
|
if (stream != null)
|
|
{
|
|
var body = request.GetRequestStream();
|
|
BytesUtility.Read(stream, body, RequestProgress, Block);
|
|
}
|
|
else if (data != null)
|
|
{
|
|
request.ContentLength = data.LongLength;
|
|
var body = request.GetRequestStream();
|
|
BytesUtility.Write(body, data, RequestProgress, Block);
|
|
}
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
/// <exception cref="System.ArgumentNullException"></exception>
|
|
/// <exception cref="System.InvalidOperationException"></exception>
|
|
/// <exception cref="System.NotSupportedException"></exception>
|
|
/// <exception cref="System.ObjectDisposedException"></exception>
|
|
/// <exception cref="System.Net.ProtocolViolationException"></exception>
|
|
/// <exception cref="System.Net.WebException"></exception>
|
|
void Parse(HttpWebResponse response)
|
|
{
|
|
if (response == null) return;
|
|
|
|
ResponseStatus = ((int)response.StatusCode).ToString();
|
|
ResponseCached = response.IsFromCache;
|
|
|
|
var headers = new StringPairs();
|
|
foreach (var key in response.Headers.AllKeys)
|
|
{
|
|
var values = response.Headers.GetValues(key);
|
|
headers.Add(key, values.Join(","));
|
|
}
|
|
ResponseHeaders = headers;
|
|
|
|
var cb = new ArrayBuilder<Cookie>();
|
|
foreach (var item in response.Cookies)
|
|
{
|
|
var cookie = item as Cookie;
|
|
if (cookie == null) continue;
|
|
cb.Add(cookie);
|
|
}
|
|
|
|
var body = response.GetResponseStream();
|
|
var progress = ResponseProgress;
|
|
var hasProgress = progress != null;
|
|
var stream = ResponseStream;
|
|
if (stream == null) ResponseData = BytesUtility.Read(body);
|
|
else BytesUtility.Read(body, stream, progress, Block);
|
|
|
|
response.Close();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region static
|
|
|
|
const int SimpleTimeout = 30000;
|
|
|
|
/// <summary>GET</summary>
|
|
public static HttpClient Get(string url, int timeout = 30000)
|
|
{
|
|
var http = new HttpClient();
|
|
http.Url = url;
|
|
http.Timeout = timeout;
|
|
http.Method = HttpMethod.GET;
|
|
http.Send();
|
|
return http;
|
|
}
|
|
|
|
/// <summary>POST</summary>
|
|
public static HttpClient Post(string url, byte[] data, int timeout = 30000, string type = "application/octet-stream")
|
|
{
|
|
var http = new HttpClient();
|
|
http.Url = url;
|
|
http.Timeout = timeout;
|
|
http.Method = HttpMethod.POST;
|
|
if (!string.IsNullOrEmpty(type)) http.RequestContentType = type;
|
|
if (data != null)
|
|
{
|
|
http.RequestContentLength = data.LongLength;
|
|
http.RequestData = data;
|
|
}
|
|
http.Send();
|
|
return http;
|
|
}
|
|
|
|
/// <summary>POST text/plain</summary>
|
|
public static HttpClient Text(string url, string text, int timeout = 30000, string type = "text/plain")
|
|
{
|
|
return Post(url, TextUtility.Bytes(text), timeout, type);
|
|
}
|
|
|
|
/// <summary>POST application/x-www-form-urlencoded</summary>
|
|
public static HttpClient Form(string url, IDictionary<string, string> form, int timeout = 30000)
|
|
{
|
|
if (form == null) return Post(url, BytesUtility.Empty, timeout, "application/x-www-form-urlencoded");
|
|
|
|
var cache = new List<string>();
|
|
foreach (var i in form)
|
|
{
|
|
var key = TextUtility.EncodeUrl(i.Key);
|
|
var value = TextUtility.EncodeUrl(i.Value);
|
|
cache.Add(key + "=" + value);
|
|
}
|
|
var text = string.Join("&", cache.ToArray());
|
|
var data = TextUtility.Bytes(text);
|
|
return Post(url, data, timeout, "application/x-www-form-urlencoded");
|
|
}
|
|
|
|
/// <summary>POST application/x-www-form-urlencoded</summary>
|
|
public static HttpClient Form(string url, Dictionary<string, string> form, int timeout = 30000) => Form(url, form as IDictionary<string, string>, timeout);
|
|
|
|
/// <summary>合并表单参数,不包含 Query 的 ? 符号。</summary>
|
|
public static string MergeForm(Dictionary<string, string> form)
|
|
{
|
|
if (form == null) return "";
|
|
if (form.Count < 1) return "";
|
|
var cache = new List<string>();
|
|
foreach (var i in form)
|
|
{
|
|
var key = TextUtility.EncodeUrl(i.Key);
|
|
var value = TextUtility.EncodeUrl(i.Value);
|
|
cache.Add(key + "=" + value);
|
|
}
|
|
var text = string.Join("&", cache.ToArray());
|
|
return text;
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|
|
|