aboutsummaryrefslogtreecommitdiff
path: root/Timeline/Services/QCloudCosService.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Timeline/Services/QCloudCosService.cs')
-rw-r--r--Timeline/Services/QCloudCosService.cs257
1 files changed, 257 insertions, 0 deletions
diff --git a/Timeline/Services/QCloudCosService.cs b/Timeline/Services/QCloudCosService.cs
new file mode 100644
index 00000000..f4358714
--- /dev/null
+++ b/Timeline/Services/QCloudCosService.cs
@@ -0,0 +1,257 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Timeline.Configs;
+
+namespace Timeline.Services
+{
+ public interface IQCloudCosService
+ {
+ /// <summary>
+ /// Test if an object in the bucket exists.
+ /// </summary>
+ /// <param name="bucket">The bucket name.</param>
+ /// <param name="key">The object key.</param>
+ /// <returns>True if exists. False if not.</returns>
+ Task<bool> IsObjectExists(string bucket, string key);
+
+ /// <summary>
+ /// Generate a presignated url to access the object.
+ /// </summary>
+ /// <param name="bucket">The bucket name.</param>
+ /// <param name="key">The object key.</param>
+ /// <returns>The presignated url.</returns>
+ string GenerateObjectGetUrl(string bucket, string key);
+ }
+
+ public class QCloudCosService : IQCloudCosService
+ {
+ private readonly IOptionsMonitor<QCloudCosConfig> _config;
+ private readonly ILogger<QCloudCosService> _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ public QCloudCosService(IOptionsMonitor<QCloudCosConfig> config, ILogger<QCloudCosService> logger, IHttpClientFactory httpClientFactory)
+ {
+ _config = config;
+ _logger = logger;
+ _httpClientFactory = httpClientFactory;
+ }
+
+ private const string BucketNamePattern = @"^(([a-z0-9][a-z0-9-]*[a-z0-9])|[a-z0-9])$";
+
+ public static bool ValidateBucketName(string bucketName)
+ {
+ return Regex.IsMatch(bucketName, BucketNamePattern);
+ }
+
+ public class QCloudCredentials
+ {
+ public string SecretId { get; set; }
+ public string SecretKey { get; set; }
+ }
+
+ public class RequestInfo
+ {
+ public string Method { get; set; }
+ public string Uri { get; set; }
+ public IEnumerable<KeyValuePair<string, string>> Parameters { get; set; }
+ public IEnumerable<KeyValuePair<string, string>> Headers { get; set; }
+ }
+
+ public class TimeDuration
+ {
+ public TimeDuration()
+ {
+
+ }
+
+ public TimeDuration(DateTimeOffset start, DateTimeOffset end)
+ {
+ Start = start;
+ End = end;
+ }
+
+ public DateTimeOffset Start { get; set; }
+ public DateTimeOffset End { get; set; }
+ }
+
+ public static string GenerateSign(QCloudCredentials credentials, RequestInfo request, TimeDuration signValidTime)
+ {
+ Debug.Assert(credentials != null);
+ Debug.Assert(credentials.SecretId != null);
+ Debug.Assert(credentials.SecretKey != null);
+ Debug.Assert(request != null);
+ Debug.Assert(request.Method != null);
+ Debug.Assert(request.Uri != null);
+ Debug.Assert(signValidTime != null);
+ Debug.Assert(signValidTime.Start < signValidTime.End, "Start must be before End in sign valid time.");
+
+ List<(string key, string value)> Transform(IEnumerable<KeyValuePair<string, string>> raw)
+ {
+ if (raw == null)
+ return new List<(string key, string value)>();
+
+ var sorted = raw.Select(p => (key: p.Key.ToLower(), value: WebUtility.UrlEncode(p.Value))).ToList();
+ sorted.Sort((left, right) => string.CompareOrdinal(left.key, right.key));
+ return sorted;
+ }
+
+ var transformedParameters = Transform(request.Parameters);
+ var transformedHeaders = Transform(request.Headers);
+
+ List<(string, string)> result = new List<(string, string)>();
+
+ const string signAlgorithm = "sha1";
+ result.Add(("q-sign-algorithm", signAlgorithm));
+
+ result.Add(("q-ak", credentials.SecretId));
+
+ var signTime = $"{signValidTime.Start.ToUnixTimeSeconds().ToString()};{signValidTime.End.ToUnixTimeSeconds().ToString()}";
+ var keyTime = signTime;
+ result.Add(("q-sign-time", signTime));
+ result.Add(("q-key-time", keyTime));
+
+ result.Add(("q-header-list", string.Join(';', transformedHeaders.Select(h => h.key))));
+ result.Add(("q-url-param-list", string.Join(';', transformedParameters.Select(p => p.key))));
+
+ HMACSHA1 hmac = new HMACSHA1();
+
+ string ByteArrayToString(byte[] bytes)
+ {
+ return BitConverter.ToString(bytes).Replace("-", "").ToLower();
+ }
+
+ hmac.Key = Encoding.UTF8.GetBytes(credentials.SecretKey);
+ var signKey = ByteArrayToString(hmac.ComputeHash(Encoding.UTF8.GetBytes(keyTime)));
+
+ string Join(IEnumerable<(string key, string value)> raw)
+ {
+ return string.Join('&', raw.Select(p => string.Concat(p.key, "=", p.value)));
+ }
+
+ var httpString = new StringBuilder()
+ .Append(request.Method.ToLower()).Append('\n')
+ .Append(request.Uri).Append('\n')
+ .Append(Join(transformedParameters)).Append('\n')
+ .Append(Join(transformedHeaders)).Append('\n')
+ .ToString();
+
+ string Sha1(string data)
+ {
+ var sha1 = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(data));
+ return ByteArrayToString(sha1);
+ }
+
+ var stringToSign = new StringBuilder()
+ .Append(signAlgorithm).Append('\n')
+ .Append(signTime).Append('\n')
+ .Append(Sha1(httpString)).Append('\n')
+ .ToString();
+
+ hmac.Key = Encoding.UTF8.GetBytes(signKey);
+ var signature = ByteArrayToString(hmac.ComputeHash(
+ Encoding.UTF8.GetBytes(stringToSign)));
+
+ result.Add(("q-signature", signature));
+
+ return Join(result);
+ }
+
+ private QCloudCredentials GetCredentials()
+ {
+ var config = _config.CurrentValue;
+ return new QCloudCredentials
+ {
+ SecretId = config.SecretId,
+ SecretKey = config.SecretKey
+ };
+ }
+
+ private string GetHost(string bucket)
+ {
+ var config = _config.CurrentValue;
+ return $"{bucket}-{config.AppId}.cos.{config.Region}.myqcloud.com";
+ }
+
+ public async Task<bool> IsObjectExists(string bucket, string key)
+ {
+ if (bucket == null)
+ throw new ArgumentNullException(nameof(bucket));
+ if (key == null)
+ throw new ArgumentNullException(nameof(key));
+ if (!ValidateBucketName(bucket))
+ throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket));
+
+ var client = _httpClientFactory.CreateClient();
+
+ var host = GetHost(bucket);
+ var encodedKey = WebUtility.UrlEncode(key);
+
+ var request = new HttpRequestMessage();
+ request.Method = HttpMethod.Head;
+ request.RequestUri = new Uri($"https://{host}/{encodedKey}");
+ request.Headers.Host = host;
+ request.Headers.Date = DateTimeOffset.Now;
+ request.Headers.TryAddWithoutValidation("Authorization", GenerateSign(GetCredentials(), new RequestInfo
+ {
+ Method = "head",
+ Uri = "/" + encodedKey,
+ Headers = new Dictionary<string, string>
+ {
+ ["Host"] = host
+ }
+ }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(2))));
+
+ try
+ {
+ var response = await client.SendAsync(request);
+
+ if (response.IsSuccessStatusCode)
+ return true;
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ return false;
+
+ throw new Exception($"Unknown response code. {response.ToString()}");
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "An error occured when test a cos object existence.");
+ return false;
+ }
+ }
+
+ public string GenerateObjectGetUrl(string bucket, string key)
+ {
+ if (bucket == null)
+ throw new ArgumentNullException(nameof(bucket));
+ if (key == null)
+ throw new ArgumentNullException(nameof(key));
+ if (!ValidateBucketName(bucket))
+ throw new ArgumentException($"Bucket name is not valid. Param is {bucket} .", nameof(bucket));
+
+ var host = GetHost(bucket);
+ var encodedKey = WebUtility.UrlEncode(key);
+
+ var signature = GenerateSign(GetCredentials(), new RequestInfo
+ {
+ Method = "get",
+ Uri = "/" + encodedKey,
+ Headers = new Dictionary<string, string>
+ {
+ ["Host"] = host
+ }
+ }, new TimeDuration(DateTimeOffset.Now, DateTimeOffset.Now.AddMinutes(6)));
+
+ return $"https://{host}/{encodedKey}?{signature}";
+ }
+ }
+}