-
-
Notifications
You must be signed in to change notification settings - Fork 319
HTTP Interactions #1999
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
Merged
Merged
HTTP Interactions #1999
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
d1761a6
basic structure
Plerx2493 e9b4b55
Improve Bindings, implement VerifySignature and implement DiscordHttp…
Plerx2493 02ec5df
Merge branch 'refs/heads/master' into plerx/dev/HttpInteractions
Plerx2493 7f2b391
Add http interaction injection
Plerx2493 69502d8
Respond to pings
Plerx2493 d57ccd1
Correctly parse the interaction
Plerx2493 4dea04c
remove nested sln
Plerx2493 cc5a953
Make interaction cancelble to cancel on http request cancelation
Plerx2493 9b53d73
Add AspNetCore extension and add verifykey to DiscordApplication
Plerx2493 97e02c5
Add missing xml docs
Plerx2493 0780610
fix formatting, project name and csproj config
Plerx2493 e4fb171
Add PackageId
Plerx2493 01b075b
Remove RootNamespace from csproj
Plerx2493 2716487
Maybe write the bytes directly, untested
Plerx2493 1dfef85
Apply namespace rename and remove unused usings
Plerx2493 73b98d2
Package description and tags and also reword method description
Plerx2493 4370c19
explicitly add outputtype and add another package tag
Plerx2493 555165c
Better exception if discord closed the connection
Plerx2493 2dbe5fb
Merge branch 'refs/heads/master' into plerx/dev/HttpInteractions
Plerx2493 2859fc2
fix merge mistake
Plerx2493 787eec6
remove NL
Plerx2493 3d5f2f3
Add CancellationToken in HandleDiscordInteractionAsync in aspnet package
Plerx2493 d2e2f83
another missing ct
Plerx2493 96ad68c
and another ct
Plerx2493 4b17461
Take ArraySegments to allow more optimizations
Plerx2493 623e752
XMLDocs and formatting
Plerx2493 948d790
add mising xmldoc
Plerx2493 96c1765
Parse the interaction like we do it in D*spatch
Plerx2493 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
DSharpPlus.HttpInteractions.AspNetCore/DSharpPlus.HttpInteractions.AspNetCore.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
| <PropertyGroup> | ||
| <OutputType>Library</OutputType> | ||
| <PackageId>DSharpPlus.HttpInteractions.AspNetCore</PackageId> | ||
| <Description>A package to easily use http based discord interactions with DSharpPlus in a Asp.Net project</Description> | ||
| <PackageTags>$(PackageTags), interactions, slash-commands, http-interactions</PackageTags> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <IsPackable>true</IsPackable> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <ProjectReference Include="..\DSharpPlus\DSharpPlus.csproj"/> | ||
| </ItemGroup> | ||
| </Project> |
78 changes: 78 additions & 0 deletions
78
DSharpPlus.HttpInteractions.AspNetCore/EndpointRouteBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| using System.Buffers; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Net.Mime; | ||
|
|
||
| using DSharpPlus.Net.HttpInteractions; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.Extensions.Primitives; | ||
| using Microsoft.Net.Http.Headers; | ||
|
|
||
| namespace DSharpPlus.HttpInteractions.AspNetCore; | ||
|
|
||
| public static class EndpointRouteBuilderExtensions | ||
| { | ||
| /// <summary> | ||
| /// Registers an endpoint to handle HTTP-based interactions from Discord | ||
| /// </summary> | ||
| /// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns> | ||
| public static RouteHandlerBuilder AddDiscordHttpInteractions | ||
| ( | ||
| this IEndpointRouteBuilder builder, | ||
| [StringSyntax("Route")] string url = "/interactions" | ||
| ) | ||
| => builder.MapPost(url, HandleDiscordInteractionAsync); | ||
|
|
||
| private static async Task HandleDiscordInteractionAsync(HttpContext httpContext, CancellationToken cancellationToken, [FromServices] DiscordClient client) | ||
| { | ||
| if (!httpContext.Request.Headers.TryGetValue(HeaderNames.ContentLength, out StringValues lengthString) | ||
| || !int.TryParse(lengthString, out int length)) | ||
| { | ||
| httpContext.Response.StatusCode = 400; | ||
| return; | ||
| } | ||
|
|
||
| byte[] bodyBuffer = ArrayPool<byte>.Shared.Rent(length); | ||
| await httpContext.Request.Body.ReadExactlyAsync(bodyBuffer.AsMemory(..length), cancellationToken); | ||
|
|
||
| if (!TryExtractHeaders(httpContext.Request.Headers, out string? timestamp, out string? key)) | ||
| { | ||
| httpContext.Response.StatusCode = 401; | ||
| return; | ||
| } | ||
|
|
||
| if (!DiscordHeaders.VerifySignature(bodyBuffer.AsSpan(..length), timestamp!, key!, client.CurrentApplication.VerifyKey)) | ||
| { | ||
| httpContext.Response.StatusCode = 401; | ||
| return; | ||
| } | ||
|
|
||
| ArraySegment<byte> body = new(bodyBuffer, 0, length); | ||
|
|
||
| byte[] result = await client.HandleHttpInteractionAsync(body, cancellationToken); | ||
|
|
||
| ArrayPool<byte>.Shared.Return(bodyBuffer); | ||
|
|
||
| httpContext.Response.StatusCode = 200; | ||
| httpContext.Response.ContentLength = result.Length; | ||
| httpContext.Response.ContentType = MediaTypeNames.Application.Json; | ||
|
|
||
| await httpContext.Response.Body.WriteAsync(result, cancellationToken); | ||
| } | ||
|
|
||
| public static bool TryExtractHeaders(IDictionary<string, StringValues> headers, out string? timestamp, out string? key) | ||
| { | ||
| timestamp = null; | ||
| key = null; | ||
| if (headers.TryGetValue(DiscordHeaders.TimestampHeaderName, out StringValues svTimestamp)) | ||
| { | ||
| timestamp = svTimestamp; | ||
| } | ||
|
|
||
| if (headers.TryGetValue(DiscordHeaders.SignatureHeaderName, out StringValues svKey)) | ||
| { | ||
| key = svKey; | ||
| } | ||
|
|
||
| return timestamp is not null && key is not null; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| using System; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using System.Threading.Tasks; | ||
|
|
||
| using DSharpPlus.Net.Abstractions; | ||
| using DSharpPlus.Net.Serialization; | ||
| using Newtonsoft.Json; | ||
|
|
||
| namespace DSharpPlus.Entities; | ||
|
|
||
| public class DiscordHttpInteraction : DiscordInteraction | ||
| { | ||
| [JsonIgnore] | ||
| internal readonly TaskCompletionSource taskCompletionSource = new(); | ||
|
|
||
| [JsonIgnore] | ||
| internal byte[] response; | ||
|
|
||
| internal bool Cancel() => this.taskCompletionSource.TrySetCanceled(); | ||
|
|
||
| internal async Task<byte[]> GetResponseAsync() | ||
| { | ||
| await this.taskCompletionSource.Task; | ||
|
|
||
| return this.response; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public override Task CreateResponseAsync(DiscordInteractionResponseType type, DiscordInteractionResponseBuilder? builder = null) | ||
| { | ||
| if (this.taskCompletionSource.Task.IsCanceled) | ||
| { | ||
| throw new InvalidOperationException( | ||
| "Discord closed the connection. This is likely due to exeeding the limit of 3 seconds to the response."); | ||
| } | ||
|
|
||
| if (this.ResponseState is not DiscordInteractionResponseState.Unacknowledged) | ||
Plerx2493 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| throw new InvalidOperationException("A response has already been made to this interaction."); | ||
| } | ||
|
|
||
| this.ResponseState = type == DiscordInteractionResponseType.DeferredChannelMessageWithSource | ||
| ? DiscordInteractionResponseState.Deferred | ||
| : DiscordInteractionResponseState.Replied; | ||
|
|
||
| DiscordInteractionResponsePayload payload = new() | ||
| { | ||
| Type = type, | ||
| Data = builder is not null | ||
| ? new DiscordInteractionApplicationCommandCallbackData | ||
| { | ||
| Content = builder.Content, | ||
| Title = builder.Title, | ||
| CustomId = builder.CustomId, | ||
| Embeds = builder.Embeds, | ||
| IsTTS = builder.IsTTS, | ||
| Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false), | ||
| Flags = builder.Flags, | ||
| Components = builder.Components, | ||
| Choices = builder.Choices, | ||
| Poll = builder.Poll?.BuildInternal(), | ||
| } | ||
| : null | ||
| }; | ||
|
|
||
| this.response = Encoding.UTF8.GetBytes(DiscordJson.SerializeObject(payload)); | ||
| this.taskCompletionSource.SetResult(); | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| using System; | ||
| using System.Buffers; | ||
| using System.Text; | ||
| using DSharpPlus.Entities; | ||
|
|
||
| namespace DSharpPlus.Net.HttpInteractions; | ||
|
|
||
| public class DiscordHeaders | ||
| { | ||
| /// <summary> | ||
| /// Name of the HTTP header which contains the timestamp of the signature | ||
| /// </summary> | ||
| public const string TimestampHeaderName = "x-signature-timestamp"; | ||
|
|
||
| /// <summary> | ||
| /// Name of the HTTP header which contains the signature | ||
| /// </summary> | ||
| public const string SignatureHeaderName = "x-signature-ed25519"; | ||
|
|
||
| /// <summary> | ||
| /// Verifies the signature of a http interaction. | ||
| /// </summary> | ||
| /// <param name="body">Raw http body</param> | ||
| /// <param name="timestamp">Timestamp header sent by discord. <see cref="TimestampHeaderName"/></param> | ||
| /// <param name="signingKey">Signing key sent by discord. <see cref="SignatureHeaderName"/></param> | ||
| /// <param name="publicKey"> | ||
| /// Public key of the application this interaction was sent. | ||
| /// This key can be accessed at DiscordApplication. | ||
| /// <see cref="DiscordApplication.VerifyKey"/> | ||
| /// </param> | ||
| /// <returns>Indicates if this signature is valid.</returns> | ||
| public static bool VerifySignature(ReadOnlySpan<byte> body, string timestamp, string signingKey, string publicKey) | ||
| { | ||
| byte[] timestampBytes = Encoding.UTF8.GetBytes(timestamp); | ||
| byte[] publicKeyBytes = Convert.FromHexString(publicKey); | ||
| byte[] signatureBytes = Convert.FromHexString(signingKey); | ||
|
|
||
| int messageLength = body.Length + timestampBytes.Length; | ||
| byte[] message = ArrayPool<byte>.Shared.Rent(messageLength); | ||
|
|
||
| timestampBytes.CopyTo(message, 0); | ||
| body.CopyTo(message.AsSpan(timestampBytes.Length)); | ||
|
|
||
| bool result = Ed25519.TryVerifySignature(message.AsSpan(..messageLength), publicKeyBytes.AsSpan(), signatureBytes.AsSpan()); | ||
|
|
||
| ArrayPool<byte>.Shared.Return(message); | ||
|
|
||
| return result; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace DSharpPlus.Net.HttpInteractions; | ||
|
|
||
| internal static partial class Ed25519 | ||
| { | ||
| internal const int SignatureBytes = 64; | ||
| internal const int PublicKeyBytes = 32; | ||
|
|
||
| internal static unsafe bool TryVerifySignature(ReadOnlySpan<byte> body, ReadOnlySpan<byte> publicKey, ReadOnlySpan<byte> signature) | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNotEqual(signature.Length, SignatureBytes); | ||
| ArgumentOutOfRangeException.ThrowIfNotEqual(publicKey.Length, PublicKeyBytes); | ||
|
|
||
| fixed (byte* signaturePtr = signature) | ||
| fixed (byte* messagePtr = body) | ||
| fixed (byte* publicKeyPtr = publicKey) | ||
| { | ||
| return Bindings.crypto_sign_ed25519_verify_detached(signaturePtr, messagePtr, (ulong)body.Length, publicKeyPtr) == 0; | ||
| } | ||
| } | ||
|
|
||
| // Ed25519.Bindings is a nested type to lazily load sodium. the native load is done by the static constructor, | ||
| // which will not be executed unless this code actually gets used. since we cannot rely on sodium being present at all | ||
| // times, it is imperative this remains a nested type. | ||
| private static partial class Bindings | ||
| { | ||
| static Bindings() | ||
| { | ||
| if (sodium_init() == -1) | ||
| { | ||
| throw new InvalidOperationException("Failed to initialize libsodium."); | ||
| } | ||
| } | ||
|
|
||
| [LibraryImport("sodium")] | ||
| private static unsafe partial int sodium_init(); | ||
|
|
||
| [LibraryImport("sodium")] | ||
| internal static unsafe partial int crypto_sign_ed25519_verify_detached(byte* signature, byte* message, ulong messageLength, byte* publicKey); | ||
akiraveliara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't necessarily indicative of Discord closing the connection though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cancelation token should be tied to the http connection. Maybe i should add additional docs, in aspnet you get a ct which is cancled when the request is cancled
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but in general it can be that the dev cancels the interaction with other intentions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think velvet means that the http connection could also be closed due to other reasons, such as a random network drop?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its the common case.