fix(discord): sanitize embed join links
This commit is contained in:
@@ -272,14 +272,16 @@ public sealed class DiscordPlatformMessenger : IPlatformMessenger
|
|||||||
? "—"
|
? "—"
|
||||||
: string.Join(", ", notification.ConfirmedPlayers.Select(p => Mention(p.User)));
|
: string.Join(", ", notification.ConfirmedPlayers.Select(p => Mention(p.User)));
|
||||||
|
|
||||||
return new EmbedProperties()
|
var embed = new EmbedProperties()
|
||||||
.WithTitle($"Ссылка на игру: {notification.Title}")
|
.WithTitle($"Ссылка на игру: {notification.Title}")
|
||||||
.WithDescription(
|
.WithDescription(
|
||||||
$"Время: **{notification.ScheduledAt.FormatMoscow()}** (МСК)\n" +
|
$"Время: **{notification.ScheduledAt.FormatMoscow()}** (МСК)\n" +
|
||||||
$"Ссылка: {notification.JoinLink}\n\n" +
|
$"Ссылка: {notification.JoinLink}\n\n" +
|
||||||
$"Участники: {mentions}")
|
$"Участники: {mentions}")
|
||||||
.WithUrl(notification.JoinLink)
|
|
||||||
.WithColor(new Color(0x57F287));
|
.WithColor(new Color(0x57F287));
|
||||||
|
|
||||||
|
var embedUrl = DiscordEmbedUrls.NormalizeHttpUrl(notification.JoinLink);
|
||||||
|
return embedUrl is null ? embed : embed.WithUrl(embedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<ActionRowProperties> BuildRsvpRows(Guid sessionId, bool disabled)
|
private static IReadOnlyList<ActionRowProperties> BuildRsvpRows(Guid sessionId, bool disabled)
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
namespace GmRelay.DiscordBot.Rendering;
|
||||||
|
|
||||||
|
public static class DiscordEmbedUrls
|
||||||
|
{
|
||||||
|
public static string? NormalizeHttpUrl(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var candidate = value.Trim();
|
||||||
|
if (IsSupportedHttpUrl(candidate, out var normalized))
|
||||||
|
return normalized;
|
||||||
|
|
||||||
|
if (candidate.Contains("://", StringComparison.Ordinal))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return IsSupportedHttpUrl($"https://{candidate}", out normalized)
|
||||||
|
&& HasPublicHost(normalized)
|
||||||
|
? normalized
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSupportedHttpUrl(string value, out string normalized)
|
||||||
|
{
|
||||||
|
normalized = string.Empty;
|
||||||
|
|
||||||
|
if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized = uri.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasPublicHost(string value) =>
|
||||||
|
Uri.TryCreate(value, UriKind.Absolute, out var uri)
|
||||||
|
&& uri.Host.Contains('.', StringComparison.Ordinal);
|
||||||
|
}
|
||||||
@@ -70,9 +70,10 @@ public static class DiscordSessionBatchRenderer
|
|||||||
.WithInline()
|
.WithInline()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(session.JoinLink))
|
var embedUrl = DiscordEmbedUrls.NormalizeHttpUrl(session.JoinLink);
|
||||||
|
if (embedUrl is not null)
|
||||||
{
|
{
|
||||||
embed = embed.WithUrl(session.JoinLink);
|
embed = embed.WithUrl(embedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
embed = embed.WithColor(GetColor(session));
|
embed = embed.WithColor(GetColor(session));
|
||||||
|
|||||||
@@ -176,6 +176,32 @@ public sealed class DiscordSessionBatchRendererTests
|
|||||||
Assert.Equal("https://example.com/game", embeds[0].Url);
|
Assert.Equal("https://example.com/game", embeds[0].Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Render_ShouldNormalizeBareDomainJoinLinkForEmbedUrl()
|
||||||
|
{
|
||||||
|
var sessionId = Guid.NewGuid();
|
||||||
|
var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "mobaxterm.mobatek.net/game") };
|
||||||
|
var participants = Array.Empty<ParticipantBatchDto>();
|
||||||
|
|
||||||
|
var view = SessionBatchViewBuilder.Build("Test", sessions, participants);
|
||||||
|
var (embeds, _) = DiscordSessionBatchRenderer.Render(view);
|
||||||
|
|
||||||
|
Assert.Equal("https://mobaxterm.mobatek.net/game", embeds[0].Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Render_ShouldNotSetEmbedUrlWhenJoinLinkIsNotHttpUrl()
|
||||||
|
{
|
||||||
|
var sessionId = Guid.NewGuid();
|
||||||
|
var sessions = new[] { new SessionBatchDto(sessionId, DateTime.UtcNow, SessionStatus.Planned, 4, "test") };
|
||||||
|
var participants = Array.Empty<ParticipantBatchDto>();
|
||||||
|
|
||||||
|
var view = SessionBatchViewBuilder.Build("Test", sessions, participants);
|
||||||
|
var (embeds, _) = DiscordSessionBatchRenderer.Render(view);
|
||||||
|
|
||||||
|
Assert.Null(embeds[0].Url);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Render_ShouldEmbedCorrectFieldValues()
|
public void Render_ShouldEmbedCorrectFieldValues()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user