diff --git a/DisCatSharp.Tests/SafetyTests/DisCatSharp.SafetyTests.csproj b/DisCatSharp.Tests/SafetyTests/DisCatSharp.SafetyTests.csproj index bde63f6e99..8be25edd34 100644 --- a/DisCatSharp.Tests/SafetyTests/DisCatSharp.SafetyTests.csproj +++ b/DisCatSharp.Tests/SafetyTests/DisCatSharp.SafetyTests.csproj @@ -6,8 +6,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DisCatSharp/Entities/DiscordSignedLink.cs b/DisCatSharp/Entities/DiscordSignedLink.cs new file mode 100644 index 0000000000..622c3ca239 --- /dev/null +++ b/DisCatSharp/Entities/DiscordSignedLink.cs @@ -0,0 +1,85 @@ +using System; +using System.Globalization; +using System.Web; + +namespace DisCatSharp.Entities; + +/// +/// Represents a used for attachments and other things to improve security +/// and prevent bad actors from abusing Discord's CDN. +/// +public class DiscordSignedLink : Uri +{ + /// + /// When the signed link expires. + /// + public DateTimeOffset? ExpiresAt { get; init; } + + /// + /// When the signed link was generated. + /// + public DateTimeOffset? IssuedAt { get; init; } + + /// + /// The signature of the signed link. + /// + public string? Signature { get; init; } + + /// + /// Initializes a new instance of the class with the specified URI for signed discord links. + /// + /// An . + public DiscordSignedLink(Uri uri) + : base(uri.AbsoluteUri) + { + ArgumentNullException.ThrowIfNull(uri); + + if (string.IsNullOrWhiteSpace(this.Query)) + return; + + var queries = HttpUtility.ParseQueryString(this.Query); + + if (!queries.HasKeys()) + return; + + if (queries.Get("ex") is { } expiresString && long.TryParse(expiresString, NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out var expiresTimeStamp)) + this.ExpiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresTimeStamp); + + if (queries.Get("is") is { } issuedString && + long.TryParse(issuedString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var issuedTimeStamp)) + this.IssuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedTimeStamp); + + this.Signature = queries.Get("hm"); + } + + /// + /// Initializes a new instance of the class with the specified URI for signed discord links. + /// + /// A string that identifies the resource to be represented by the instance. + public DiscordSignedLink(string uriString) + : base(uriString) + { + ArgumentNullException.ThrowIfNull(uriString); + + if (string.IsNullOrWhiteSpace(this.Query)) + return; + + var queries = HttpUtility.ParseQueryString(this.Query); + + if (!queries.HasKeys()) + return; + + if (queries.Get("ex") is { } expiresString && long.TryParse(expiresString, NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out var expiresTimeStamp)) + this.ExpiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresTimeStamp); + + if (queries.Get("is") is { } issuedString && + long.TryParse(issuedString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var issuedTimeStamp)) + this.IssuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedTimeStamp); + + this.Signature = queries.Get("hm"); + } +} diff --git a/DisCatSharp/Entities/DiscordUri.cs b/DisCatSharp/Entities/DiscordUri.cs index aab5e77ea5..f9efeed3c0 100644 --- a/DisCatSharp/Entities/DiscordUri.cs +++ b/DisCatSharp/Entities/DiscordUri.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace DisCatSharp.Net; +namespace DisCatSharp.Entities; /// /// An URI in a Discord embed doesn't necessarily conform to the RFC 3986. If it uses the attachment:// @@ -11,7 +11,7 @@ namespace DisCatSharp.Net; /// Discord. /// [JsonConverter(typeof(DiscordUriJsonConverter))] -public class DiscordUri +public sealed class DiscordUri : DiscordSignedLink { private readonly object _value; @@ -25,6 +25,7 @@ public class DiscordUri /// /// The value. internal DiscordUri(Uri value) + : base(value) { this._value = value ?? throw new ArgumentNullException(nameof(value)); this.Type = DiscordUriType.Standard; @@ -35,12 +36,13 @@ internal DiscordUri(Uri value) /// /// The value. internal DiscordUri(string value) + : base(value) { ArgumentNullException.ThrowIfNull(value); if (IsStandard(value)) { - this._value = new Uri(value); + this._value = new DiscordSignedLink(value); this.Type = DiscordUriType.Standard; } else @@ -74,7 +76,7 @@ public Uri ToUri() => this.Type == DiscordUriType.Standard ? this._value as Uri : throw new UriFormatException( - $@"DiscordUri ""{this._value}"" would be invalid as a regular URI, please the {nameof(this.Type)} property first."); + $@"DiscordUri ""{this._value}"" would be invalid as a regular URI, please set the correct {nameof(this.Type)} property first."); /// /// Represents a uri json converter. @@ -87,7 +89,8 @@ internal sealed class DiscordUriJsonConverter : JsonConverter /// The writer. /// The value. /// The serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue((value as DiscordUri)._value); + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + => writer.WriteValue((value as DiscordUri)._value); /// /// Reads the json. diff --git a/DisCatSharp/Entities/Embed/DiscordEmbedVideo.cs b/DisCatSharp/Entities/Embed/DiscordEmbedVideo.cs index b302099716..f1be456627 100644 --- a/DisCatSharp/Entities/Embed/DiscordEmbedVideo.cs +++ b/DisCatSharp/Entities/Embed/DiscordEmbedVideo.cs @@ -13,7 +13,7 @@ public sealed class DiscordEmbedVideo : ObservableApiObject /// Gets the source url of the video. /// [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri Url { get; internal set; } + public DiscordSignedLink Url { get; internal set; } /// /// Gets the height of the video. diff --git a/DisCatSharp/Entities/Message/DiscordAttachment.cs b/DisCatSharp/Entities/Message/DiscordAttachment.cs index c2695f1a73..51bf71e27b 100644 --- a/DisCatSharp/Entities/Message/DiscordAttachment.cs +++ b/DisCatSharp/Entities/Message/DiscordAttachment.cs @@ -37,13 +37,13 @@ public class DiscordAttachment : NullableSnowflakeObject /// Gets the URL of the file. /// [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string Url { get; internal set; } + public DiscordSignedLink Url { get; internal set; } /// /// Gets the proxied URL of the file. /// [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] - public string ProxyUrl { get; internal set; } + public DiscordSignedLink ProxyUrl { get; internal set; } /// /// Gets the height. Applicable only if the attachment is an image.