This commit is contained in:
Dmitri Shimanski
2025-07-30 01:37:10 +03:00
parent 2d36a60f5d
commit 8c212321e0
8 changed files with 74 additions and 17 deletions

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageReference Include="NUnit" Version="4.2.2"/>
<PackageReference Include="NUnit.Analyzers" Version="4.3.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
</Project>

View File

@@ -8,6 +8,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aoyo.Taiga.Tests", "Aoyo.Taiga.Tests\Aoyo.Taiga.Tests.csproj", "{D36CA783-EA93-4B54-B3A4-B911B00D0887}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Debug|Any CPU.Build.0 = Debug|Any CPU {CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Release|Any CPU.ActiveCfg = Release|Any CPU {CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Release|Any CPU.Build.0 = Release|Any CPU {CAFC85B2-1606-44DF-A3CD-9DEAA07ADB78}.Release|Any CPU.Build.0 = Release|Any CPU
{D36CA783-EA93-4B54-B3A4-B911B00D0887}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D36CA783-EA93-4B54-B3A4-B911B00D0887}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D36CA783-EA93-4B54-B3A4-B911B00D0887}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D36CA783-EA93-4B54-B3A4-B911B00D0887}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -1,11 +1,14 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using Aoyo.Taiga.WebHookTypes; using Aoyo.Taiga.WebHookTypes;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace Aoyo.Taiga.Controllers; namespace Aoyo.Taiga.Controllers;
@@ -40,17 +43,20 @@ public class TaigaWebHook : Controller
{ {
try try
{ {
var signature = Request.Headers["X-TAIGA-WEBHOOK-SIGNATURE"].First()!; var signature = Request.Headers["X-TAIGA-WEBHOOK-SIGNATURE"].First();
if (signature == null) return BadRequest("Provide signature first");
using var reader = new StreamReader(Request.Body); using var reader = new StreamReader(Request.Body);
var data = await reader.ReadToEndAsync(); var data = await reader.ReadToEndAsync();
_logger.LogInformation(data); _logger.LogInformation(data);
var hash = VerifySignature(_key, data);
if (_config.GetValue<string>("ASPNETCORE_ENVIRONMENT") == "Production")
if (!VerifySignature(_key, data, signature))
{ if (!string.Equals(signature, hash, StringComparison.OrdinalIgnoreCase))
return BadRequest("Invalid signature"); {
} return BadRequest($"Invalid signature {hash}");
}
var webhookType = DetermineWebhookType(data); var webhookType = DetermineWebhookType(data);
@@ -66,6 +72,8 @@ public class TaigaWebHook : Controller
case "task": case "task":
await HandleTaskWebhook(data); await HandleTaskWebhook(data);
break; break;
case "test":
return Ok("test success!");
default: default:
_logger.LogWarning("Unsupported type of webhook: {WebhookType}", webhookType); _logger.LogWarning("Unsupported type of webhook: {WebhookType}", webhookType);
return BadRequest($"Unsupported type of webhook: {webhookType}"); return BadRequest($"Unsupported type of webhook: {webhookType}");
@@ -79,14 +87,13 @@ public class TaigaWebHook : Controller
return BadRequest(exception.Message); return BadRequest(exception.Message);
} }
} }
private bool VerifySignature(string key, string data, string signature) private string VerifySignature(string key, string data)
{ {
using HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key)); using var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(data)); var hashBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(data));
var computedHash = Convert.ToHexString(hashBytes).ToLower(); var computedSignature = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
return computedSignature;
return computedHash.Length == signature.Length;
} }
private async Task HandleUserStoryWebhook(string json) private async Task HandleUserStoryWebhook(string json)
@@ -386,13 +393,13 @@ public class TaigaWebHook : Controller
if (_logChannel == null) if (_logChannel == null)
{ {
_logger.LogError("Не удалось получить Discord канал с ID: {ChannelId}", _id); _logger.LogError("Can`t get channel with ID: {ChannelId}", _id);
return; return;
} }
await _logChannel.SendMessageAsync(embed: await _logChannel.SendMessageAsync(embed:
new EmbedBuilder() new EmbedBuilder()
.WithAuthor(payload.By.Username, payload.By.Photo) .WithAuthor(payload.By.Username, payload.By.GravatarId != null ? "https://gravatar.com/avatar/" + payload.By.GravatarId : payload.By.Photo)
.WithTitle($"{payload.Action} {payload.Type.ToLower()}") .WithTitle($"{payload.Action} {payload.Type.ToLower()}")
.WithDescription(payload.Description) .WithDescription(payload.Description)
.WithColor(payload.Color) .WithColor(payload.Color)

View File

@@ -22,7 +22,7 @@ WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y curl && \ apt-get install -y curl && \
rm -rf /var/lib/apt/lists/* \ rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/aoyo/health || exit 1 HEALTHCHECK --interval=30s --timeout=3s --retries=3 CMD curl -f http://localhost:8080/aoyo/health || exit 1

View File

@@ -9,6 +9,11 @@ public class Program
WebHost. WebHost.
CreateDefaultBuilder(args). CreateDefaultBuilder(args).
UseStartup<Startup>(). UseStartup<Startup>().
ConfigureAppConfiguration(k =>
k.
AddJsonFile("appsettings.json").
AddJsonFile("appsettings.Development.json").
AddEnvironmentVariables()).
UseKestrel( UseKestrel(
l => l =>
l.ListenAnyIP(8080) l.ListenAnyIP(8080)

View File

@@ -18,4 +18,6 @@ public class TaigaUser
[JsonPropertyName("photo")] [JsonPropertyName("photo")]
public string Photo { get; set; } public string Photo { get; set; }
[JsonPropertyName("gravatar_id")]
public string? GravatarId { get; set; }
} }

View File

@@ -4,5 +4,12 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"Discord": {
"Token": "as",
"Id": 0
},
"Taiga": {
"Key": "123"
} }
} }

7
compose.yaml Normal file
View File

@@ -0,0 +1,7 @@
services:
aoyo.taiga:
image: aoyo.taiga
build:
context: .
dockerfile: Aoyo.Taiga/Dockerfile