Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signed Attachments #457

Merged
merged 2 commits into from Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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