初始化
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
//
|
||||
// HttpRequestMessageBuilder.cs
|
||||
//
|
||||
// Author:
|
||||
// MiNG <developer@ming.gz.cn>
|
||||
//
|
||||
// Copyright (c) 2018 Alibaba Cloud
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Aliyun.Api.LogService.Infrastructure.Authentication;
|
||||
using Aliyun.Api.LogService.Utils;
|
||||
using Google.Protobuf;
|
||||
using Ionic.Zlib;
|
||||
using LZ4;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Aliyun.Api.LogService.Infrastructure.Protocol.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Builder for constructing the <see cref="HttpRequestMessage"/>.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public class HttpRequestMessageBuilder : IRequestBuilder<HttpRequestMessage>
|
||||
{
|
||||
private static readonly Byte[] EmptyByteArray = new Byte[0];
|
||||
|
||||
private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
private readonly HttpRequestMessage httpRequestMessage;
|
||||
|
||||
private readonly Encoding encoding;
|
||||
|
||||
private readonly String path;
|
||||
|
||||
private readonly IDictionary<String, String> query;
|
||||
|
||||
/// <summary>
|
||||
/// The authentication credential.
|
||||
/// </summary>
|
||||
private Credential credential;
|
||||
|
||||
/// <summary>
|
||||
/// The real content to transfer.
|
||||
/// </summary>
|
||||
private Object content;
|
||||
|
||||
/// <summary>
|
||||
/// The Content-MD5 header in HEX format.
|
||||
/// </summary>
|
||||
private String contentMd5Hex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the serialized content.
|
||||
/// </summary>
|
||||
private Byte[] SerializedContent =>
|
||||
this.content == null
|
||||
? null
|
||||
: this.content as Byte[]
|
||||
?? throw new InvalidOperationException("Content must serialized before this operation.");
|
||||
|
||||
/// <summary>
|
||||
/// Proceed the actions after content prepared (i.e., all transforms (e.g., serialize, compress, encrypt, encode) of <see cref="content"/> are applied).
|
||||
/// </summary>
|
||||
private Action contentHandler;
|
||||
|
||||
/// <summary>
|
||||
/// The signature type.
|
||||
/// </summary>
|
||||
private SignatureType signatureType;
|
||||
|
||||
public HttpRequestMessageBuilder(HttpMethod method, String uri)
|
||||
{
|
||||
this.httpRequestMessage = new HttpRequestMessage(method, uri);
|
||||
this.encoding = Encoding.UTF8;
|
||||
ParseUri(uri, out this.path, out this.query);
|
||||
|
||||
this.FillDefaultHeaders();
|
||||
}
|
||||
|
||||
private static void ParseUri(String uri, out String path, out IDictionary<String, String> query)
|
||||
{
|
||||
var absUri = new Uri(new Uri("http://fa.ke"), uri);
|
||||
path = absUri.AbsolutePath;
|
||||
query = absUri.ParseQueryString()
|
||||
.ToEnumerable()
|
||||
.ToDictionary(kv => kv.Key, kv => kv.Value); // NOTE: Restricted mode, key cannot be duplicated.
|
||||
}
|
||||
|
||||
private void FillDefaultHeaders()
|
||||
{
|
||||
this.httpRequestMessage.Headers.Date = DateTimeOffset.Now;
|
||||
this.httpRequestMessage.Headers.UserAgent.Add(new ProductInfoHeaderValue("log-dotnetcore-sdk", Constants.AssemblyVersion));
|
||||
this.httpRequestMessage.Headers.Add(LogHeaders.ApiVersion, "0.6.0");
|
||||
}
|
||||
|
||||
#region Query
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Query(String key, String value)
|
||||
{
|
||||
this.query.Add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Query(Object queryModel)
|
||||
{
|
||||
foreach (var kv in JObject.FromObject(queryModel, JsonSerializer.CreateDefault(JsonSerializerSettings)))
|
||||
{
|
||||
this.query.Add(kv.Key, kv.Value.Value<String>());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Header
|
||||
|
||||
/// <summary>
|
||||
/// Set headers of <see cref="T:System.Net.Http.Headers.HttpRequestHeaders" />
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public IRequestBuilder<HttpRequestMessage> Header(String key, String value)
|
||||
{
|
||||
this.httpRequestMessage.Headers.Add(key, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void ContentHeader(Action<HttpContentHeaders> option)
|
||||
{
|
||||
if (this.httpRequestMessage.Content == null)
|
||||
{
|
||||
this.contentHandler += () => option(this.httpRequestMessage.Content.Headers);
|
||||
} else
|
||||
{
|
||||
option(this.httpRequestMessage.Content.Headers);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBodyRawSize(Int32 size)
|
||||
=> this.httpRequestMessage.Headers.Add(LogHeaders.BodyRawSize, size.ToString());
|
||||
|
||||
private void SetCompressType(String compressType)
|
||||
=> this.httpRequestMessage.Headers.Add(LogHeaders.CompressType, compressType);
|
||||
|
||||
private void SetSignatureMethod(String signatureMethod)
|
||||
=> this.httpRequestMessage.Headers.Add(LogHeaders.SignatureMethod, signatureMethod);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Content
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Content(Byte[] content)
|
||||
=> this.Content((Object) content);
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Content(Object content)
|
||||
{
|
||||
this.content = content;
|
||||
|
||||
if (content is Byte[] data)
|
||||
{
|
||||
this.SetBodyRawSize(data.Length);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Serialize
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Serialize(SerializeType serializeType)
|
||||
{
|
||||
switch (this.content)
|
||||
{
|
||||
case null:
|
||||
throw new InvalidOperationException("Nothing to serialize.");
|
||||
case Byte[] _:
|
||||
throw new InvalidOperationException("Content has already been serialized.");
|
||||
}
|
||||
|
||||
switch (serializeType)
|
||||
{
|
||||
case SerializeType.Json:
|
||||
{
|
||||
this.ContentHeader(x => x.ContentType = new MediaTypeHeaderValue("application/json"));
|
||||
var json = JsonConvert.SerializeObject(this.content, JsonSerializerSettings);
|
||||
this.Content(this.encoding.GetBytes(json));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SerializeType.Protobuf:
|
||||
{
|
||||
if (!(this.content is IMessage protoMessage))
|
||||
{
|
||||
throw new ArgumentException("Serialization of ProtoBuf requires IMessage.");
|
||||
}
|
||||
|
||||
this.ContentHeader(x => x.ContentType = new MediaTypeHeaderValue("application/x-protobuf"));
|
||||
this.Content(protoMessage.ToByteArray());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(serializeType), serializeType, null);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion Serialize
|
||||
|
||||
|
||||
#region Compress
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Compress(CompressType compressType)
|
||||
{
|
||||
if (this.SerializedContent == null)
|
||||
{
|
||||
throw new InvalidOperationException("Nothing to compress.");
|
||||
}
|
||||
|
||||
switch (compressType)
|
||||
{
|
||||
case CompressType.None:
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
case CompressType.Lz4:
|
||||
{
|
||||
this.SetCompressType("lz4");
|
||||
this.content = LZ4Codec.Encode(this.SerializedContent, 0, this.SerializedContent.Length);
|
||||
break;
|
||||
}
|
||||
|
||||
case CompressType.Deflate:
|
||||
{
|
||||
this.SetCompressType("deflate");
|
||||
this.content = ZlibStream.CompressBuffer(this.SerializedContent);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(compressType), compressType, null);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion Compress
|
||||
|
||||
|
||||
#region Authentication
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Authenticate(Credential credential)
|
||||
{
|
||||
Ensure.NotNull(credential, nameof(credential));
|
||||
Ensure.NotEmpty(credential.AccessKeyId, nameof(credential.AccessKeyId));
|
||||
Ensure.NotEmpty(credential.AccessKey, nameof(credential.AccessKey));
|
||||
|
||||
this.credential = credential;
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Sign
|
||||
|
||||
public IRequestBuilder<HttpRequestMessage> Sign(SignatureType signatureType)
|
||||
{
|
||||
this.signatureType = signatureType;
|
||||
return this;
|
||||
}
|
||||
|
||||
private Byte[] ComputeSignature()
|
||||
{
|
||||
switch (this.signatureType)
|
||||
{
|
||||
case SignatureType.HmacSha1:
|
||||
{
|
||||
using (var hasher = new HMACSHA1(this.encoding.GetBytes(this.credential.AccessKey)))
|
||||
{
|
||||
this.SetSignatureMethod("hmac-sha1"); // This header must be set before generating sign source.
|
||||
var signSource = this.GenerateSignSource();
|
||||
var sign = hasher.ComputeHash(this.encoding.GetBytes(signSource));
|
||||
|
||||
return sign;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(this.signatureType), this.signatureType, "Currently only support [hmac-sha1] signature.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String GenerateSignSource()
|
||||
{
|
||||
var verb = this.httpRequestMessage.Method.Method;
|
||||
var contentMd5 = this.contentMd5Hex;
|
||||
var contentType = this.httpRequestMessage.Content?.Headers.ContentType.MediaType;
|
||||
var date = this.httpRequestMessage.Headers.Date?.ToString("r"); /* RFC 822 format */
|
||||
var logHeaders = String.Join("\n", this.httpRequestMessage.Headers
|
||||
.Concat(this.httpRequestMessage.Content?.Headers ?? Enumerable.Empty<KeyValuePair<String, IEnumerable<String>>>())
|
||||
.Where(x => x.Key.StartsWith("x-log") || x.Key.StartsWith("x-acs"))
|
||||
.Select(x => new KeyValuePair<String, String>(x.Key.ToLower(), x.Value.SingleOrDefault() /* Fault tolerance */))
|
||||
.Where(x => x.Value.IsNotEmpty()) // Remove empty header
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => $"{x.Key}:{x.Value}"));
|
||||
|
||||
var resource = this.httpRequestMessage.RequestUri.OriginalString;
|
||||
|
||||
String signSource;
|
||||
if (this.query.IsEmpty())
|
||||
{
|
||||
signSource = String.Join("\n", verb, contentMd5 ?? String.Empty, contentType ?? String.Empty, date, logHeaders, resource);
|
||||
} else
|
||||
{
|
||||
signSource = String.Join("\n", verb, contentMd5 ?? String.Empty, contentType ?? String.Empty, date, logHeaders, resource) + "?" +
|
||||
String.Join("&", this.query
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => $"{x.Key}={x.Value}"));
|
||||
}
|
||||
return signSource;
|
||||
}
|
||||
|
||||
private Byte[] CalculateContentMd5()
|
||||
{
|
||||
using (var hasher = MD5.Create())
|
||||
{
|
||||
return hasher.ComputeHash(this.SerializedContent);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Signature
|
||||
|
||||
public HttpRequestMessage Build()
|
||||
{
|
||||
// Validate
|
||||
Ensure.NotNull(this.credential, nameof(this.credential));
|
||||
Ensure.NotEmpty(this.credential.AccessKeyId, nameof(this.credential.AccessKeyId));
|
||||
Ensure.NotEmpty(this.credential.AccessKey, nameof(this.credential.AccessKey));
|
||||
|
||||
// Process sts-token.
|
||||
var hasSecurityToken = this.httpRequestMessage.Headers.TryGetValues(LogHeaders.SecurityToken, out var securityTokens)
|
||||
&& securityTokens.FirstOrDefault().IsNotEmpty();
|
||||
|
||||
if (!hasSecurityToken && this.credential.StsToken.IsNotEmpty())
|
||||
{
|
||||
this.httpRequestMessage.Headers.Add(LogHeaders.SecurityToken, this.credential.StsToken);
|
||||
}
|
||||
|
||||
// NOTE: If x-log-bodyrawsize is empty, fill it with "0". Otherwise, some method call will be corrupted.
|
||||
if (!this.httpRequestMessage.Headers.Contains(LogHeaders.BodyRawSize))
|
||||
{
|
||||
this.SetBodyRawSize(0);
|
||||
}
|
||||
|
||||
// Build content if necessary
|
||||
if (this.SerializedContent.IsNotEmpty())
|
||||
{
|
||||
this.httpRequestMessage.Content = new ByteArrayContent(this.SerializedContent);
|
||||
this.contentHandler?.Invoke();
|
||||
|
||||
// Prepare header
|
||||
this.ContentHeader(x =>
|
||||
{
|
||||
// Compute actual length
|
||||
x.ContentLength = this.SerializedContent.Length;
|
||||
// Compute actual MD5
|
||||
this.contentMd5Hex = BitConverter.ToString(this.CalculateContentMd5()).Replace("-", String.Empty);
|
||||
|
||||
x.Add("Content-MD5", this.contentMd5Hex); // Non-standard header
|
||||
});
|
||||
} else if (this.httpRequestMessage.Method == HttpMethod.Post || this.httpRequestMessage.Method == HttpMethod.Put)
|
||||
{
|
||||
// When content is empty as well as method is `POST` or `PUT`, generate an empty content and corresponding headers.
|
||||
|
||||
this.httpRequestMessage.Content = new ByteArrayContent(EmptyByteArray);
|
||||
// Don't invoke `contentHandler` here!
|
||||
|
||||
/*
|
||||
* NOTE:
|
||||
* Here is a annoying hack, the log service service cannot accept empty `Content-Type`
|
||||
* header when POST or PUT methods. So, we have to force set some header value.
|
||||
*/
|
||||
this.ContentHeader(x =>
|
||||
{
|
||||
x.ContentType = new MediaTypeHeaderValue("application/json");
|
||||
// For some reason, I think it is better to set `Content-Type` to `0` to prevent
|
||||
// some unexpected behavior on server side.
|
||||
x.ContentLength = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Do signature
|
||||
var signature = Convert.ToBase64String(this.ComputeSignature());
|
||||
this.httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("LOG", $"{this.credential.AccessKeyId}:{signature}");
|
||||
|
||||
// Rebuild the RequestUri
|
||||
var queryString = String.Join("&", this.query
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => $"{encodeUrl(x.Key)}={encodeUrl(x.Value)}"));
|
||||
var pathAndQuery = queryString.IsNotEmpty() ? $"{this.path}?{queryString}" : this.path;
|
||||
this.httpRequestMessage.RequestUri = new Uri(pathAndQuery, UriKind.Relative);
|
||||
|
||||
return this.httpRequestMessage;
|
||||
}
|
||||
|
||||
private String encodeUrl(String value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string encoded = HttpUtility.UrlEncode(value, this.encoding);
|
||||
return encoded.Replace("+", "%20").Replace("*", "%2A").Replace("~", "%7E").Replace("/", "%2F");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user