网站首页 网站源码
using System.Buffers;
using System.Web;
using Dpz.Core.Public.Entity;
using Dpz.Core.WebApi.Models.EventRequest;
using MediatR;
using Microsoft.Extensions.Primitives;
#nullable enable
namespace Dpz.Core.WebApi.EventHandles;
/// <summary>
/// 处理又拍云回调通知
/// </summary>
public class UpyunNotificationEventHandle(
ILogger<UpyunNotificationEventHandle> logger,
UpyunOperator upyunOperator,
IConfiguration configuration,
IObjectStorageOperation objectStorageService
) : IRequestHandler<UpyunNotificationRequest, IActionResult>
{
/// <summary>
/// handle upyun notification
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<IActionResult> Handle(
UpyunNotificationRequest request,
CancellationToken cancellationToken
)
{
if (!await CheckSignature(request.Request))
{
return new UnauthorizedResult();
}
var body = await ReadBodyAsync(request.Request);
logger.LogInformation(
"receive upyun notification: {Body},authorization:{Authorization},content-type:{ContentType}",
body,
request.Request.Headers.Authorization,
request.Request.Headers.ContentType
);
var parameters = HttpUtility.ParseQueryString(body);
var bucketName = parameters.Get("bucket_name");
if (string.IsNullOrEmpty(bucketName) || bucketName != upyunOperator.Bucket)
{
return new UnauthorizedResult();
}
var statusCode = parameters.Get("status_code");
if (string.IsNullOrEmpty(statusCode) || statusCode != "200")
{
return new BadRequestResult();
}
var sourceUrl = parameters.Get("media_uris[0]");
if (!string.IsNullOrEmpty(sourceUrl))
{
await objectStorageService.DeleteAsync(sourceUrl);
}
var meta = GetVideoMetaInformation(parameters);
// todo save meta
return new NoContentResult();
}
private async Task<bool> CheckSignature(HttpRequest request)
{
var authorization = request.Headers.Authorization;
var requestDate = request.Headers.Date;
var contentMd5 = request.Headers.ContentMD5;
if (
StringValues.IsNullOrEmpty(authorization)
|| StringValues.IsNullOrEmpty(requestDate)
|| StringValues.IsNullOrEmpty(contentMd5)
)
{
return false;
}
var notifyUrl = configuration["NotifyUrl"] ?? throw new InvalidConfigurationException();
var uri = new Uri(notifyUrl);
// Method、URI、Date、Content-MD5 值,用英文符号 & 拼接为字符串
var signatureBody = $"POST&{uri.OriginalString}&{requestDate}&{contentMd5}";
var signatureKeyBytes = Encoding.UTF8.GetBytes(
upyunOperator.Password ?? throw new InvalidConfigurationException()
);
using var hmac = new HMACSHA1(signatureKeyBytes);
var signatureBodyBytes = Encoding.UTF8.GetBytes(signatureBody);
await using var stream = ApplicationTools.MemoryStreamManager.GetStream(signatureBodyBytes);
var signatureBytes = await hmac.ComputeHashAsync(stream);
var signature = Convert.ToBase64String(signatureBytes);
return authorization == $"UPYUN {upyunOperator.Operator}:{signature}";
}
private static async Task<string> ReadBodyAsync(HttpRequest request)
{
var body = new StringBuilder();
var reader = request.BodyReader;
while (true)
{
var readResult = await reader.ReadAsync();
var buffer = readResult.Buffer;
if (readResult.IsCompleted && buffer.Length > 0)
{
AppendString(body, in buffer);
}
reader.AdvanceTo(buffer.Start, buffer.End);
if (readResult.IsCompleted)
{
break;
}
}
return body.ToString();
}
private static void AppendString(
StringBuilder result,
in ReadOnlySequence<byte> readOnlySequence
)
{
var span = readOnlySequence.IsSingleSegment
? readOnlySequence.First.Span
: readOnlySequence.ToArray().AsSpan();
result.Append(Encoding.UTF8.GetString(span));
}
private readonly JsonSerializerOptions _options = new() { PropertyNameCaseInsensitive = true };
private Notification? GetVideoMetaInformation(NameValueCollection parameters)
{
var info = parameters.Get("info");
Notification? notification = null;
if (string.IsNullOrEmpty(info))
{
return notification;
}
var bytes = DpzAppBuilderExtensions.Base64StringToBytes(info);
if (bytes.Length <= 0)
{
return notification;
}
var json = Encoding.UTF8.GetString(bytes);
try
{
notification = JsonSerializer.Deserialize<Notification>(json, _options);
}
catch (Exception e)
{
logger.LogError(e, "parse notification info failed: {Json}", json);
return notification;
}
logger.LogInformation("notification: {@Notification}", notification);
return notification;
}
}
// https://www.yuque.com/u160746/fk96qt/flqrx8#u8uiM
/*
*
avopt=%2Fht%2F5%2Fs%2F1080p%2816%3A9%29&bucket_name=images-dpangzi&description=OK&info=eyJzdHJlYW1zIjpbeyJiaXRfZGVwdGgiOjgsImNvZGVjIjoiaDI2NCIsImNvZGVjX2Rlc2MiOiJILjI2NCAvIEFWQyAvIE1QRUctNCBBVkMgLyBNUEVHLTQgcGFydCAxMCIsImNvbG9yX3NwYWNlIjoiYnQ3MDkiLCJpbmRleCI6MCwibWV0YWRhdGEiOnsidmFyaWFudF9iaXRyYXRlIjoiMCJ9LCJwaXhfZm10IjoieXV2NDIwcCIsInNhbXBsZV9hc3BlY3RfcmF0aW8iOiIxOjEiLCJ0eXBlIjoidmlkZW8iLCJ2aWRlb19mcHMiOjI1LCJ2aWRlb19oZWlnaHQiOjEwODAsInZpZGVvX3dpZHRoIjoxOTIwfSx7ImF1ZGlvX2NoYW5uZWxzIjoyLCJhdWRpb19zYW1wbGVyYXRlIjo0ODAwMCwiY29kZWMiOiJhYWMiLCJjb2RlY19kZXNjIjoiQUFDIChBZHZhbmNlZCBBdWRpbyBDb2RpbmcpIiwiaW5kZXgiOjEsIm1ldGFkYXRhIjp7InZhcmlhbnRfYml0cmF0ZSI6IjAifSwidHlwZSI6ImF1ZGlvIn1dLCJmb3JtYXQiOnsiZHVyYXRpb24iOjEwNS44OCwiZnVsbG5hbWUiOiJBcHBsZSBIVFRQIExpdmUgU3RyZWFtaW5nIiwiYml0cmF0ZSI6MTE4LCJmaWxlc2l6ZSI6MTU0OCwiZm9ybWF0IjoiaGxzIn19&media_uris%5B0%5D=%2FSourceVideo%2F%E3%80%8A%E6%88%91%E5%9C%A8%E9%82%A3%E4%B8%80%E8%A7%92%E8%90%BD%E6%82%A3%E8%BF%87%E4%BC%A4%E9%A3%8E%E3%80%8B+%28%E7%A5%9E%E9%BE%99%E5%A3%AB%E5%8A%9B%E6%9E%B6%29.mp4&path%5B0%5D=%2FVideo%2F%E6%88%91%E5%9C%A8%E9%82%A3%E4%B8%80%E8%A7%92%E8%90%BD%E6%82%A3%E8%BF%87%E4%BC%A4%E9%A3%8E%2F1080p.m3u8&signature=15bfe38ab83185c23ca863fcb1fdcd3d&status_code=200&task_id=2cb5818b43820daeafadb9a271717a5e×tamp=1733469346&ts=%2FVideo%2F%E6%88%91%E5%9C%A8%E9%82%A3%E4%B8%80%E8%A7%92%E8%90%BD%E6%82%A3%E8%BF%87%E4%BC%A4%E9%A3%8E%2Fts&uploads%5B0%5D=%2FVideo%2F%E6%88%91%E5%9C%A8%E9%82%A3%E4%B8%80%E8%A7%92%E8%90%BD%E6%82%A3%E8%BF%87%E4%BC%A4%E9%A3%8E%2F1080p.m3u8
*
*/
/*
*
{
"streams": [
{
"bit_depth": 8,
"codec": "h264",
"codec_desc": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"color_space": "bt709",
"index": 0,
"metadata": {
"variant_bitrate": "0"
},
"pix_fmt": "yuv420p",
"sample_aspect_ratio": "1:1",
"type": "video",
"video_fps": 25,
"video_height": 1080,
"video_width": 1920
},
{
"audio_channels": 2,
"audio_samplerate": 48000,
"codec": "aac",
"codec_desc": "AAC (Advanced Audio Coding)",
"index": 1,
"metadata": {
"variant_bitrate": "0"
},
"type": "audio"
}
],
"format": {
"duration": 105.88,
"fullname": "Apple HTTP Live Streaming",
"bitrate": 118,
"filesize": 1548,
"format": "hls"
}
}
*
*/
上述代码是一个 ASP.NET Core Web API 的事件处理程序,专门用于处理又拍云(Upyun)的回调通知。以下是代码的主要功能和结构的详细解释:
UpyunNotificationEventHandle
: 这是一个处理又拍云通知的事件处理程序,继承自 IRequestHandler<UpyunNotificationRequest, IActionResult>
,表示它处理 UpyunNotificationRequest
类型的请求并返回 IActionResult
类型的结果。Handle
方法: 这是处理请求的主要方法。它执行以下步骤:CheckSignature
方法验证请求的签名。如果签名不正确,返回 UnauthorizedResult
。ReadBodyAsync
方法读取请求的主体内容。HttpUtility.ParseQueryString
解析请求体中的参数。bucket_name
参数是否与配置中的桶名匹配。如果不匹配,返回 UnauthorizedResult
。status_code
是否为 "200"。如果不是,返回 BadRequestResult
。media_uris[0]
存在,调用 objectStorageService.DeleteAsync
删除该 URL。GetVideoMetaInformation
方法获取视频的元信息,并在 TODO 注释中提到需要保存这些元信息。CheckSignature
方法: 验证请求的签名是否有效。它从请求头中提取 Authorization
、Date
和 Content-MD5
,并根据这些信息生成一个签名,与请求中的签名进行比较。ReadBodyAsync
方法: 异步读取请求的主体内容,使用 StringBuilder
来构建最终的字符串。GetVideoMetaInformation
方法: 从参数中提取 info
字段,并将其解码为 JSON 格式,最后反序列化为 Notification
对象。如果解析失败,会记录错误信息。AppendString
方法: 辅助方法,用于将 ReadOnlySequence<byte>
转换为字符串并追加到 StringBuilder
中。JsonSerializerOptions
: 用于 JSON 反序列化时的配置,设置为属性名不区分大小写。整体上,这段代码实现了一个完整的回调处理流程,确保了请求的合法性,处理了视频上传的通知,并提供了日志记录和错误处理机制。它是一个典型的 Web API 事件处理程序,适用于处理外部服务的回调通知。