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

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
}
}