Skip to content

Commit

Permalink
Signed Attachments (#457)
Browse files Browse the repository at this point in the history
* feat: discord signed url

* chore(deps): update microsoft.codeanalysis
  • Loading branch information
Lulalaby committed Mar 4, 2024
1 parent 5585b22 commit ef0ecfd
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 10 deletions.
4 changes: 2 additions & 2 deletions DisCatSharp.Tests/SafetyTests/DisCatSharp.SafetyTests.csproj
Expand Up @@ -6,8 +6,8 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.9.2" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
85 changes: 85 additions & 0 deletions DisCatSharp/Entities/DiscordSignedLink.cs
@@ -0,0 +1,85 @@
using System;
using System.Globalization;
using System.Web;

namespace DisCatSharp.Entities;

/// <summary>
/// Represents a <see cref="DiscordSignedLink"/> used for attachments and other things to improve security
/// and prevent bad actors from abusing Discord's CDN.
/// </summary>
public class DiscordSignedLink : Uri
{
/// <summary>
/// When the signed link expires.
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }

/// <summary>
/// When the signed link was generated.
/// </summary>
public DateTimeOffset? IssuedAt { get; init; }

/// <summary>
/// The signature of the signed link.
/// </summary>
public string? Signature { get; init; }

/// <summary>
/// Initializes a new instance of the <see cref="Uri"/> class with the specified URI for signed discord links.
/// </summary>
/// <param name="uri">An <see cref="Uri"/>.</param>
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");
}

/// <summary>
/// Initializes a new instance of the <see cref="Uri"/> class with the specified URI for signed discord links.
/// </summary>
/// <param name="uriString">A string that identifies the resource to be represented by the <see cref="Uri"/> instance.</param>
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");
}
}
13 changes: 8 additions & 5 deletions DisCatSharp/Entities/DiscordUri.cs
Expand Up @@ -3,15 +3,15 @@

using Newtonsoft.Json;

namespace DisCatSharp.Net;
namespace DisCatSharp.Entities;

/// <summary>
/// An URI in a Discord embed doesn't necessarily conform to the RFC 3986. If it uses the <c>attachment://</c>
/// protocol, it mustn't contain a trailing slash to be interpreted correctly as an embed attachment reference by
/// Discord.
/// </summary>
[JsonConverter(typeof(DiscordUriJsonConverter))]
public class DiscordUri
public sealed class DiscordUri : DiscordSignedLink
{
private readonly object _value;

Expand All @@ -25,6 +25,7 @@ public class DiscordUri
/// </summary>
/// <param name="value">The value.</param>
internal DiscordUri(Uri value)
: base(value)
{
this._value = value ?? throw new ArgumentNullException(nameof(value));
this.Type = DiscordUriType.Standard;
Expand All @@ -35,12 +36,13 @@ internal DiscordUri(Uri value)
/// </summary>
/// <param name="value">The value.</param>
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
Expand Down Expand Up @@ -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.");

/// <summary>
/// Represents a uri json converter.
Expand All @@ -87,7 +89,8 @@ internal sealed class DiscordUriJsonConverter : JsonConverter
/// <param name="writer">The writer.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The serializer.</param>
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);

/// <summary>
/// Reads the json.
Expand Down
2 changes: 1 addition & 1 deletion DisCatSharp/Entities/Embed/DiscordEmbedVideo.cs
Expand Up @@ -13,7 +13,7 @@ public sealed class DiscordEmbedVideo : ObservableApiObject
/// Gets the source url of the video.
/// </summary>
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)]
public Uri Url { get; internal set; }
public DiscordSignedLink Url { get; internal set; }

/// <summary>
/// Gets the height of the video.
Expand Down
4 changes: 2 additions & 2 deletions DisCatSharp/Entities/Message/DiscordAttachment.cs
Expand Up @@ -37,13 +37,13 @@ public class DiscordAttachment : NullableSnowflakeObject
/// Gets the URL of the file.
/// </summary>
[JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)]
public string Url { get; internal set; }
public DiscordSignedLink Url { get; internal set; }

/// <summary>
/// Gets the proxied URL of the file.
/// </summary>
[JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)]
public string ProxyUrl { get; internal set; }
public DiscordSignedLink ProxyUrl { get; internal set; }

/// <summary>
/// Gets the height. Applicable only if the attachment is an image.
Expand Down

0 comments on commit ef0ecfd

Please sign in to comment.