Added WebhookHostedService, modified Publisher and Sender
This commit is contained in:
parent
5305ad1018
commit
2dcf6b8225
@ -6,5 +6,6 @@ namespace ASC.Webhooks.Dao.Models
|
||||
{
|
||||
public int TenantId { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string SecretKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,5 +11,6 @@ namespace ASC.Webhooks.Dao.Models
|
||||
public string Data { get; set; }
|
||||
public DateTime CreationTime { get; set; }
|
||||
public EventName Event { get; set; }
|
||||
public ProcessStatus Status { get; set; }
|
||||
}
|
||||
}
|
||||
|
15
common/ASC.Webhooks/Dao/Models/WebhooksQueueEntry.cs
Normal file
15
common/ASC.Webhooks/Dao/Models/WebhooksQueueEntry.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ASC.Webhooks.Dao.Models
|
||||
{
|
||||
public class WebhooksQueueEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Data { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public string SecretKey { get; set; }
|
||||
}
|
||||
}
|
@ -45,6 +45,11 @@ namespace ASC.Webhooks.Dao
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("URI")
|
||||
.HasDefaultValueSql("''");
|
||||
|
||||
entity.Property(e => e.SecretKey)
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("SecretKey")
|
||||
.HasDefaultValueSql("''");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<WebhooksPayload>(entity =>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using ASC.Common;
|
||||
@ -33,5 +34,28 @@ namespace ASC.Webhooks
|
||||
{
|
||||
return webhooksContext.WebhooksConfigs.Where(t => t.TenantId == tenant).Select(it => it.Uri).ToList();
|
||||
}
|
||||
|
||||
public List<WebhooksConfig> GetWebhookConfigs(int tenant)
|
||||
{
|
||||
return webhooksContext.WebhooksConfigs.Where(t => t.TenantId == tenant).ToList();
|
||||
}
|
||||
|
||||
public void UpdateStatus(int id, ProcessStatus status)
|
||||
{
|
||||
var webhook = webhooksContext.WebhooksPayloads.Where(t => t.Id == id).FirstOrDefault();
|
||||
webhook.Status = status;
|
||||
webhooksContext.WebhooksPayloads.Update(webhook);
|
||||
webhooksContext.SaveChanges();
|
||||
}
|
||||
|
||||
public List<WebhooksQueueEntry> GetWebhookQueue()
|
||||
{
|
||||
return webhooksContext.WebhooksPayloads
|
||||
.Where(t => t.Status == ProcessStatus.InProcess)
|
||||
.Join(webhooksContext.WebhooksConfigs, t => t.TenantId, t => t.TenantId, (payload, config) => new { payload, config })
|
||||
.Select(t => new WebhooksQueueEntry { Id = t.payload.Id, Data = t.payload.Data, SecretKey = t.config.SecretKey, Uri = t.config.Uri })
|
||||
.OrderBy(t => t.Id)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
67
common/ASC.Webhooks/WebhookHostedService.cs
Normal file
67
common/ASC.Webhooks/WebhookHostedService.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Core;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.Webhooks
|
||||
{
|
||||
public class WebhookHostedService : BackgroundService
|
||||
{
|
||||
private ILog Log { get; }
|
||||
private DbWorker DbWorker { get; }
|
||||
private TenantManager TenantManager { get; }
|
||||
private WebhookSender WebhookSender { get; }
|
||||
|
||||
public WebhookHostedService(IOptionsMonitor<ILog> option,
|
||||
DbWorker dbWorker,
|
||||
TenantManager tenantManager,
|
||||
WebhookSender webhookSender)
|
||||
{
|
||||
Log = option.Get("ASC.Webhooks");
|
||||
DbWorker = dbWorker;
|
||||
TenantManager = tenantManager;
|
||||
WebhookSender = webhookSender;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
Log.Debug(
|
||||
$"WebhookHostedService is starting.{Environment.NewLine}");
|
||||
|
||||
stoppingToken.Register(() =>
|
||||
Log.Debug($"WebhookHostedService is stopping."));
|
||||
|
||||
await BackgroundProcessing(stoppingToken);
|
||||
}
|
||||
|
||||
private async Task BackgroundProcessing(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var webhooks = DbWorker.GetWebhookQueue();
|
||||
|
||||
foreach (var wh in webhooks)
|
||||
{
|
||||
WebhookSender.Send(wh);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("ERROR: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Core;
|
||||
using ASC.Webhooks.Dao.Models;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace ASC.Webhooks
|
||||
{
|
||||
[Scope]
|
||||
@ -21,29 +24,37 @@ namespace ASC.Webhooks
|
||||
WebhookSender = webhookSender;
|
||||
}
|
||||
|
||||
public void Publish(EventName name)
|
||||
public void Publish(EventName eventName, object data)
|
||||
{
|
||||
var result = WebhookSender.Send(name);
|
||||
var content = JsonSerializer.Serialize(data);
|
||||
|
||||
if (result)
|
||||
var tenantId = TenantManager.GetCurrentTenant().TenantId;
|
||||
var webhookConfigs = DbWorker.GetWebhookConfigs(tenantId);
|
||||
foreach (var config in webhookConfigs)
|
||||
{
|
||||
var tenantId = TenantManager.GetCurrentTenant().TenantId;
|
||||
|
||||
var webhooksPayload = new WebhooksPayload
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Event = name,
|
||||
Event = eventName,
|
||||
CreationTime = DateTime.UtcNow,
|
||||
Data = JsonSerializer.Serialize(name),
|
||||
Data = content,
|
||||
Status = ProcessStatus.InProcess
|
||||
};
|
||||
|
||||
DbWorker.WriteToJournal(webhooksPayload);
|
||||
}
|
||||
}
|
||||
}
|
||||
public struct EventName
|
||||
public enum EventName
|
||||
{
|
||||
public const string NewUserRegistered = "NewUserRegistered";
|
||||
public const string TenantDeleted = "TenantDeleted";
|
||||
NewFileCreated,
|
||||
FileUpdated
|
||||
}
|
||||
|
||||
public enum ProcessStatus
|
||||
{
|
||||
InProcess,
|
||||
Success,
|
||||
Failed
|
||||
}
|
||||
}
|
||||
|
@ -1,66 +1,84 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ASC.Common;
|
||||
using ASC.Common.Logging;
|
||||
using ASC.Core;
|
||||
using ASC.Webhooks.Dao.Models;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ASC.Webhooks
|
||||
{
|
||||
[Scope]
|
||||
public class WebhookSender
|
||||
{
|
||||
private const int repeatCount = 5;
|
||||
|
||||
private static readonly HttpClient httpClient = new HttpClient();
|
||||
private ILog Log { get; }
|
||||
private DbWorker DbWorker { get; }
|
||||
private TenantManager TenantManager { get; }
|
||||
public WebhookSender(DbWorker dbWorker, TenantManager tenantManager)
|
||||
public WebhookSender(IOptionsMonitor<ILog> option, DbWorker dbWorker)
|
||||
{
|
||||
DbWorker = dbWorker;
|
||||
TenantManager = tenantManager;
|
||||
Log = option.Get("ASC.Webhooks");
|
||||
}
|
||||
|
||||
public bool Send(EventName eventName)
|
||||
public async Task Send(WebhooksQueueEntry webhooksQueueEntry)
|
||||
{
|
||||
var tenantId = TenantManager.GetCurrentTenant().TenantId;
|
||||
var requestURIList = DbWorker.GetWebhookUri(tenantId);
|
||||
foreach (var requestUrl in requestURIList)
|
||||
var URI = webhooksQueueEntry.Uri;
|
||||
var secretKey = webhooksQueueEntry.SecretKey;
|
||||
|
||||
for (int i = 0; i < repeatCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var webRequest = WebRequest.Create(requestUrl);
|
||||
webRequest.Method = "POST";
|
||||
webRequest.ContentType = "application/json";
|
||||
webRequest.Headers.Add("Secret", GetSecret("secretKey"));
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, URI);
|
||||
request.Headers.Add("Accept", "*/*");
|
||||
request.Headers.Add("Secret","SHA256=" + GetSecretHash(secretKey, webhooksQueueEntry.Data));//*retry
|
||||
|
||||
var data = JsonSerializer.Serialize(eventName);
|
||||
request.Content = new StringContent(
|
||||
webhooksQueueEntry.Data,
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var encoding = new UTF8Encoding();
|
||||
byte[] bytes = encoding.GetBytes(data);
|
||||
webRequest.ContentLength = bytes.Length;
|
||||
using (var writeStream = webRequest.GetRequestStream())
|
||||
var response = await httpClient.SendAsync(request);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
writeStream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
using (var webResponse = webRequest.GetResponse())
|
||||
using (var reader = new StreamReader(webResponse.GetResponseStream()))
|
||||
{
|
||||
string responseFromServer = reader.ReadToEnd();
|
||||
return true;
|
||||
DbWorker.UpdateStatus(webhooksQueueEntry.Id, ProcessStatus.Success);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if(i == repeatCount)
|
||||
{
|
||||
DbWorker.UpdateStatus(webhooksQueueEntry.Id, ProcessStatus.Failed);
|
||||
}
|
||||
|
||||
Log.Error("ERROR: " + ex.Message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetSecret(string secretKey)
|
||||
private string GetSecretHash(string secretKey, string body)
|
||||
{
|
||||
return "";
|
||||
string computedSignature;
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secretKey);
|
||||
|
||||
using (var hasher = new HMACSHA256(secretBytes))
|
||||
{
|
||||
var data = Encoding.UTF8.GetBytes(body);
|
||||
computedSignature = BitConverter.ToString(hasher.ComputeHash(data));
|
||||
}
|
||||
|
||||
return computedSignature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\ASC.Api.Core\ASC.Api.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ASC.Webhooks\ASC.Webhooks.csproj" />
|
||||
<ProjectReference Include="..\Core\ASC.Files.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -57,6 +57,7 @@ using ASC.Web.Files.Services.WCFService.FileOperations;
|
||||
using ASC.Web.Files.Utils;
|
||||
using ASC.Web.Studio.Core;
|
||||
using ASC.Web.Studio.Utility;
|
||||
using ASC.Webhooks;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@ -99,6 +100,7 @@ namespace ASC.Api.Documents
|
||||
private ProductEntryPoint ProductEntryPoint { get; }
|
||||
private TenantManager TenantManager { get; }
|
||||
private FileUtility FileUtility { get; }
|
||||
private WebhookPublisher WebhookPublisher { get; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
@ -127,7 +129,8 @@ namespace ASC.Api.Documents
|
||||
ProductEntryPoint productEntryPoint,
|
||||
TenantManager tenantManager,
|
||||
FileUtility fileUtility,
|
||||
ConsumerFactory consumerFactory)
|
||||
ConsumerFactory consumerFactory,
|
||||
WebhookPublisher webhookPublisher)
|
||||
{
|
||||
FilesControllerHelperString = filesControllerHelperString;
|
||||
FilesControllerHelperInt = filesControllerHelperInt;
|
||||
@ -152,6 +155,7 @@ namespace ASC.Api.Documents
|
||||
ProductEntryPoint = productEntryPoint;
|
||||
TenantManager = tenantManager;
|
||||
FileUtility = fileUtility;
|
||||
WebhookPublisher = webhookPublisher;
|
||||
}
|
||||
|
||||
[Read("info")]
|
||||
@ -1015,7 +1019,9 @@ namespace ASC.Api.Documents
|
||||
[Create("{folderId:int}/file")]
|
||||
public FileWrapper<int> CreateFileFromBody(int folderId, [FromBody]CreateFileModel<int> model)
|
||||
{
|
||||
return FilesControllerHelperInt.CreateFile(folderId, model.Title, model.TemplateId, model.EnableExternalExt);
|
||||
var response = FilesControllerHelperInt.CreateFile(folderId, model.Title, model.TemplateId, model.EnableExternalExt);
|
||||
WebhookPublisher.Publish(EventName.NewFileCreated, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
[Create("{folderId:int}/file")]
|
||||
|
@ -82,6 +82,8 @@ using ASC.Web.Studio.UserControls.FirstTime;
|
||||
using ASC.Web.Studio.UserControls.Management;
|
||||
using ASC.Web.Studio.UserControls.Statistics;
|
||||
using ASC.Web.Studio.Utility;
|
||||
using ASC.Webhooks;
|
||||
using ASC.Webhooks.Dao.Models;
|
||||
|
||||
using Google.Authenticator;
|
||||
|
||||
@ -166,6 +168,7 @@ namespace ASC.Api.Settings
|
||||
private ILog Log { get; set; }
|
||||
private TelegramHelper TelegramHelper { get; }
|
||||
private PaymentManager PaymentManager { get; }
|
||||
private DbWorker WebhookDbWorker { get; }
|
||||
public Constants Constants { get; }
|
||||
|
||||
public SettingsController(
|
||||
@ -228,6 +231,7 @@ namespace ASC.Api.Settings
|
||||
EncryptionWorker encryptionWorker,
|
||||
PasswordHasher passwordHasher,
|
||||
PaymentManager paymentManager,
|
||||
DbWorker dbWorker,
|
||||
Constants constants)
|
||||
{
|
||||
Log = option.Get("ASC.Api");
|
||||
@ -289,6 +293,7 @@ namespace ASC.Api.Settings
|
||||
UrlShortener = urlShortener;
|
||||
TelegramHelper = telegramHelper;
|
||||
PaymentManager = paymentManager;
|
||||
WebhookDbWorker = dbWorker;
|
||||
Constants = constants;
|
||||
}
|
||||
|
||||
@ -2911,6 +2916,16 @@ namespace ASC.Api.Settings
|
||||
TelegramHelper.Disconnect(AuthContext.CurrentAccount.ID, Tenant.TenantId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add new config for webhooks
|
||||
/// </summary>
|
||||
[Create("webhook")]
|
||||
public void CreateWebhook(WebhooksConfig model)
|
||||
{
|
||||
model.TenantId = TenantManager.GetCurrentTenant().TenantId;
|
||||
WebhookDbWorker.AddWebhookConfig(model);
|
||||
}
|
||||
|
||||
private readonly int maxCount = 10;
|
||||
private readonly int expirationMinutes = 2;
|
||||
private void CheckCache(string basekey)
|
||||
|
Loading…
Reference in New Issue
Block a user