├── icon.png
├── docs
└── schema.png
├── Harpoon.Common
├── HarpoonBuilder.cs
├── Assembly.cs
├── IHarpoonBuilder.cs
├── Sender
│ ├── ISignatureService.cs
│ ├── DefaultSignatureService.cs
│ └── DefaultWebHookSender.cs
├── IQueuedProcessor.cs
├── IWebHookSender.cs
├── IWebHookNotification.cs
├── IWebHookFilter.cs
├── IWebHookService.cs
├── IWebHookStore.cs
├── WebHookNotification.cs
├── Background
│ ├── BackgroundSender.cs
│ ├── BackgroundQueue.cs
│ └── QueuedHostedService.cs
├── DefaultWebHookService.cs
├── IWebHookWorkItem.cs
├── WebHookWorkItem.cs
├── IWebHook.cs
├── Harpoon.Common.csproj
├── DefaultNotificationProcessor.cs
└── ServicesCollectionsExtensions.cs
├── Harpoon.Controllers
├── OpenApi.cs
├── Swashbuckle
│ ├── WebHookSubscriptionPointAttribute.cs
│ └── WebHookSubscriptionFilter.cs
├── Models
│ ├── WebHookFilter.cs
│ └── WebHook.cs
├── WebHookTriggersController.cs
├── Harpoon.Controllers.csproj
├── ServicesCollectionsExtensions.cs
└── WebHooksController.cs
├── Harpoon.Registrations.EFStorage
├── WebHookFilter.cs
├── IRegistrationsContext.cs
├── Harpoon.Registrations.EFStorage.csproj
├── WebHookNotification.cs
├── WebHook.cs
├── WebHookLog.cs
├── EFNotificationProcessor.cs
├── WebHookStore.cs
├── WebHookReplayService.cs
├── EFWebHookSender.cs
├── ModelBuilderExtensions.cs
└── ServicesCollectionsExtensions.cs
├── Harpoon.Registrations
├── IWebHookValidator.cs
├── IWebHookTriggerProvider.cs
├── ISecretProtector.cs
├── WebHookRegistrationStoreResult.cs
├── IPrincipalIdGetter.cs
├── Harpoon.Registrations.csproj
├── ServicesCollectionsExtensions.cs
├── WebHookTrigger.cs
├── DefaultSecretProtector.cs
├── DefaultPrincipalIdGetter.cs
├── IWebHookRegistrationStore.cs
└── DefaultWebHookValidator.cs
├── Harpoon.Tests
├── DefaultSignatureServiceTests.cs
├── Fixtures
│ ├── BackgroundSenderFixture.cs
│ ├── DatabaseFixture.cs
│ └── HostFixture.cs
├── Harpoon.Tests.csproj
├── BackgroundSenderTests.cs
├── WebHookStoreTests.cs
├── DefaultPrincipalIdGetterTests.cs
├── DefaultWebHookServiceTests.cs
├── WebHookSubscriptionFilterTests.cs
├── Mocks
│ ├── TestContext.cs
│ └── HttpClientMocker.cs
├── DefaultWebHookValidatorTests.cs
├── ServicesCollectionsExtensionsTests.cs
├── MassTransitTests.cs
├── DefaultSenderTests.cs
└── ControllersTests.cs
├── Harpoon.MassTransit
├── Consumer.cs
├── Harpoon.MassTransit.csproj
├── PublisherService.cs
└── ServicesCollectionsExtensions.cs
├── LICENSE
├── appveyor.yml
├── Harpoon.sln
└── .gitignore
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Poltuu/Harpoon/HEAD/icon.png
--------------------------------------------------------------------------------
/docs/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Poltuu/Harpoon/HEAD/docs/schema.png
--------------------------------------------------------------------------------
/Harpoon.Common/HarpoonBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Harpoon
4 | {
5 | class HarpoonBuilder : IHarpoonBuilder
6 | {
7 | public IServiceCollection Services { get; }
8 |
9 | internal HarpoonBuilder(IServiceCollection services)
10 | {
11 | Services = services;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/Harpoon.Controllers/OpenApi.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace Harpoon.Controllers
3 | {
4 | ///
5 | /// Class with OpenApi details
6 | ///
7 | public static class OpenApi
8 | {
9 | ///
10 | /// Group Name of the Harpoon controllers
11 | ///
12 | public const string GroupName = "WebHooks";
13 | }
14 | }
--------------------------------------------------------------------------------
/Harpoon.Controllers/Swashbuckle/WebHookSubscriptionPointAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon.Controllers.Swashbuckle
4 | {
5 | ///
6 | /// This attributes informs Swashbuckle to generate a callback node on this method, matching the harpoon configuration
7 | ///
8 | [AttributeUsage(AttributeTargets.Method)]
9 | public class WebHookSubscriptionPointAttribute : Attribute { }
10 | }
11 |
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/WebHookFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon.Registrations.EFStorage
4 | {
5 | ///
6 | /// Default implementation of
7 | ///
8 | public class WebHookFilter : IWebHookFilter
9 | {
10 | ///
11 | public Guid Id { get; set; }
12 |
13 | ///
14 | public string Trigger { get; set; }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Harpoon.Common/Assembly.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("Harpoon.Tests")]
4 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
--------------------------------------------------------------------------------
/Harpoon.Common/IHarpoonBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Harpoon
4 | {
5 | ///
6 | /// Harpoon configuration builder. Extensions methods should come from the same namespace.
7 | ///
8 | public interface IHarpoonBuilder
9 | {
10 | ///
11 | /// Gets the used for registration
12 | ///
13 | IServiceCollection Services { get; }
14 | }
15 | }
--------------------------------------------------------------------------------
/Harpoon.Common/Sender/ISignatureService.cs:
--------------------------------------------------------------------------------
1 | namespace Harpoon.Sender
2 | {
3 | ///
4 | /// Represents a class able to assign a signature to content using a secret
5 | ///
6 | public interface ISignatureService
7 | {
8 | ///
9 | /// Returns a unique signature for content using the provided secret
10 | ///
11 | ///
12 | ///
13 | ///
14 | string GetSignature(string secret, string content);
15 | }
16 | }
--------------------------------------------------------------------------------
/Harpoon.Common/IQueuedProcessor.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Harpoon
5 | {
6 | ///
7 | /// Represents a class able to consume workItems
8 | ///
9 | ///
10 | public interface IQueuedProcessor
11 | {
12 | ///
13 | /// Process workItem into the webhook pipeline
14 | ///
15 | ///
16 | ///
17 | ///
18 | Task ProcessAsync(T workItem, CancellationToken cancellationToken);
19 | }
20 | }
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHookSender.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Harpoon
5 | {
6 | ///
7 | /// Represents a class able to generates http calls from a
8 | ///
9 | public interface IWebHookSender
10 | {
11 | ///
12 | /// Generates an http call matching a
13 | ///
14 | ///
15 | ///
16 | ///
17 | Task SendAsync(IWebHookWorkItem webHookWorkItem, CancellationToken cancellationToken = default);
18 | }
19 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/IWebHookValidator.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Harpoon.Registrations
5 | {
6 | ///
7 | /// Represents a class able to throws an exception if an is invalid.
8 | ///
9 | public interface IWebHookValidator
10 | {
11 | ///
12 | /// Throws an exception if the given is invalid.
13 | ///
14 | ///
15 | ///
16 | ///
17 | Task ValidateAsync(IWebHook webHook, CancellationToken cancellationToken = default);
18 | }
19 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/IWebHookTriggerProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Harpoon.Registrations
4 | {
5 | ///
6 | /// Represents a class able to returns valid available for registration.
7 | ///
8 | public interface IWebHookTriggerProvider
9 | {
10 | ///
11 | /// Returns valid available for registration.
12 | /// This is used to validate registration.
13 | /// Typical implementation is expected to be in-memory
14 | ///
15 | ///
16 | IReadOnlyDictionary GetAvailableTriggers();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHookNotification.cs:
--------------------------------------------------------------------------------
1 | namespace Harpoon
2 | {
3 | ///
4 | /// Represents the content of an event that triggered
5 | ///
6 | public interface IWebHookNotification
7 | {
8 | ///
9 | /// Gets the name of the event.
10 | /// If pattern matching is used, this may contain parameters i.e. `client.23.creation`
11 | ///
12 | string TriggerId { get; }
13 |
14 | ///
15 | /// Gets an serializable object representing the payload to be sent to the registered webhooks
16 | /// This is serialized as-is using the default json serialization settings
17 | ///
18 | object Payload { get; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Harpoon.Registrations/ISecretProtector.cs:
--------------------------------------------------------------------------------
1 | namespace Harpoon.Registrations.EFStorage
2 | {
3 | ///
4 | /// A class able to protect and unprotect a piece of plain text data
5 | ///
6 | public interface ISecretProtector
7 | {
8 | ///
9 | /// Protects a piece of plain text data
10 | ///
11 | ///
12 | ///
13 | string Protect(string plaintext);
14 |
15 | ///
16 | /// Unprotects a piece of protectedData
17 | ///
18 | ///
19 | ///
20 | string Unprotect(string protectedData);
21 | }
22 | }
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHookFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon
4 | {
5 | ///
6 | /// Represents a filter on triggered events, i.e. an event type to listen to
7 | ///
8 | public interface IWebHookFilter
9 | {
10 | ///
11 | /// Gets or sets the unique identifier.
12 | ///
13 | Guid Id { get; set; }
14 |
15 | ///
16 | /// Gets a unique name for a listened event.
17 | /// Depending on the implementation, pattern matching can be used.
18 | /// For instance, '*.created' could refer to any event similar to the pattern.
19 | ///
20 | string Trigger { get; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHookService.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Harpoon
5 | {
6 | ///
7 | /// Represents a service able to handle an event and translate it appropriate webhooks.
8 | /// This is the entry point of a webhook pipeline.
9 | ///
10 | public interface IWebHookService
11 | {
12 | ///
13 | /// Start the notification process by adding the notification into a queue
14 | ///
15 | ///
16 | ///
17 | ///
18 | Task NotifyAsync(IWebHookNotification notification, CancellationToken cancellationToken = default);
19 | }
20 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/WebHookRegistrationStoreResult.cs:
--------------------------------------------------------------------------------
1 | namespace Harpoon.Registrations
2 | {
3 | ///
4 | /// This exposes the different possible outcomes during a write operation on a
5 | ///
6 | public enum WebHookRegistrationStoreResult
7 | {
8 | ///
9 | /// This value means that everything went well
10 | ///
11 | Success,
12 | ///
13 | /// This value means that a requested object was not present
14 | ///
15 | NotFound,
16 | ///
17 | /// This value means that an unexpected error was caught
18 | ///
19 | InternalError
20 | }
21 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/IPrincipalIdGetter.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Principal;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Harpoon.Registrations
6 | {
7 | ///
8 | /// Represents a class able to extract a string from a principal, that will be later used as an id for webhook registration
9 | ///
10 | public interface IPrincipalIdGetter
11 | {
12 | ///
13 | /// Extracts a string from given principal, later used as an id for webhook registration
14 | ///
15 | ///
16 | ///
17 | ///
18 | Task GetPrincipalIdAsync(IPrincipal principal, CancellationToken cancellationToken = default);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHookStore.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Harpoon
6 | {
7 | ///
8 | /// Represents a class able to return the matching a specific
9 | ///
10 | public interface IWebHookStore
11 | {
12 | ///
13 | /// Returns the matching a specific
14 | ///
15 | ///
16 | ///
17 | ///
18 | Task> GetApplicableWebHooksAsync(IWebHookNotification notification, CancellationToken cancellationToken = default);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Harpoon.Common/WebHookNotification.cs:
--------------------------------------------------------------------------------
1 | namespace Harpoon
2 | {
3 | ///
4 | /// Represents the content of an event that triggered
5 | ///
6 | public class WebHookNotification : IWebHookNotification
7 | {
8 | ///
9 | public string TriggerId { get; set; }
10 |
11 | ///
12 | public object Payload { get; set; }
13 |
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | ///
18 | ///
19 | public WebHookNotification(string trigger, object payload)
20 | {
21 | TriggerId = trigger;
22 | Payload = payload;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/IRegistrationsContext.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace Harpoon.Registrations.EFStorage
4 | {
5 | ///
6 | /// Represents a DbContext able to expose WebHooks
7 | ///
8 | public interface IRegistrationsContext
9 | {
10 | ///
11 | /// Gets the queryable entry point
12 | ///
13 | IQueryable WebHooks { get; }
14 |
15 | ///
16 | /// Gets the queryable entry point
17 | ///
18 | IQueryable WebHookNotifications { get; }
19 |
20 | ///
21 | /// Gets the queryable entry point
22 | ///
23 | IQueryable WebHookLogs { get; }
24 | }
25 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/DefaultSignatureServiceTests.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Sender;
2 | using System;
3 | using Xunit;
4 |
5 | namespace Harpoon.Tests
6 | {
7 | public class DefaultSignatureServiceTests
8 | {
9 | const string ValidSecret = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_";
10 |
11 | [Fact]
12 | public void BadSecret()
13 | {
14 | var service = new DefaultSignatureService();
15 | Assert.Throws(() => service.GetSignature(null, "content"));
16 | }
17 |
18 | [Theory]
19 | [InlineData(null)]
20 | [InlineData("")]
21 | [InlineData("content")]
22 | public void NormalScenario(string content)
23 | {
24 | var service = new DefaultSignatureService();
25 | service.GetSignature(ValidSecret, content);
26 | Assert.True(true);
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/Harpoon.Common/Background/BackgroundSender.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Harpoon.Background
6 | {
7 | internal class BackgroundSender : IWebHookSender
8 | {
9 | private readonly BackgroundQueue _webHooksQueue;
10 |
11 | public BackgroundSender(BackgroundQueue webHooksQueue)
12 | {
13 | _webHooksQueue = webHooksQueue ?? throw new ArgumentNullException(nameof(webHooksQueue));
14 | }
15 |
16 | public async Task SendAsync(IWebHookWorkItem webHookWorkItem, CancellationToken cancellationToken = default)
17 | {
18 | if (webHookWorkItem == null)
19 | {
20 | throw new ArgumentNullException(nameof(webHookWorkItem));
21 | }
22 |
23 | await _webHooksQueue.QueueWebHookAsync(webHookWorkItem);
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/Harpoon.Common/Background/BackgroundQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Channels;
4 | using System.Threading.Tasks;
5 |
6 | namespace Harpoon.Background
7 | {
8 | internal class BackgroundQueue
9 | {
10 | private readonly Channel _channel = Channel.CreateUnbounded(new UnboundedChannelOptions
11 | {
12 | SingleReader = true,
13 | SingleWriter = true
14 | });
15 |
16 | public ValueTask QueueWebHookAsync(T workItem)
17 | {
18 | if (workItem == null)
19 | {
20 | throw new ArgumentException(nameof(workItem));
21 | }
22 |
23 | return _channel.Writer.WriteAsync(workItem);
24 | }
25 |
26 | public ValueTask DequeueAsync(CancellationToken cancellationToken)
27 | {
28 | return _channel.Reader.ReadAsync(cancellationToken);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Harpoon.MassTransit/Consumer.cs:
--------------------------------------------------------------------------------
1 | using MassTransit;
2 | using System;
3 | using System.Threading.Tasks;
4 |
5 | namespace Harpoon.MassTransit
6 | {
7 | ///
8 | /// Default message consumer
9 | ///
10 | ///
11 | public class Consumer : IConsumer
12 | where TMessage : class
13 | {
14 | private readonly IQueuedProcessor _processor;
15 |
16 | /// Initializes a new instance of the class.
17 | public Consumer(IQueuedProcessor processor)
18 | {
19 | _processor = processor ?? throw new ArgumentNullException(nameof(processor));
20 | }
21 |
22 | ///
23 | public Task Consume(ConsumeContext context)
24 | => _processor.ProcessAsync(context.Message, context.CancellationToken);
25 | }
26 | }
--------------------------------------------------------------------------------
/Harpoon.Common/DefaultWebHookService.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Background;
2 | using System;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace Harpoon
7 | {
8 | internal class DefaultWebHookService : IWebHookService
9 | {
10 | private readonly BackgroundQueue _webHooksQueue;
11 |
12 | public DefaultWebHookService(BackgroundQueue webHooksQueue)
13 | {
14 | _webHooksQueue = webHooksQueue ?? throw new ArgumentNullException(nameof(webHooksQueue));
15 | }
16 |
17 | public async Task NotifyAsync(IWebHookNotification notification, CancellationToken cancellationToken = default)
18 | {
19 | if (notification == null)
20 | {
21 | throw new ArgumentNullException(nameof(notification));
22 | }
23 |
24 | await _webHooksQueue.QueueWebHookAsync(notification);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHookWorkItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon
4 | {
5 | ///
6 | /// Represents a unique work item generated by a
7 | ///
8 | public interface IWebHookWorkItem
9 | {
10 | ///
11 | /// Gets the object unique id.
12 | ///
13 | Guid Id { get; }
14 | ///
15 | /// Gets the object time stamp.
16 | ///
17 | DateTime Timestamp { get; }
18 | ///
19 | /// Gets the that generated this .
20 | ///
21 | IWebHookNotification Notification { get; }
22 | ///
23 | /// Gets the registered that matched the .
24 | ///
25 | IWebHook WebHook { get; }
26 | }
27 | }
--------------------------------------------------------------------------------
/Harpoon.Controllers/Models/WebHookFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon.Controllers.Models
4 | {
5 | ///
6 | /// Represents a filter on triggered events, i.e. an event type to listen to
7 | ///
8 | public class WebHookFilter : IWebHookFilter
9 | {
10 | ///
11 | public Guid Id { get; set; }
12 | ///
13 | public string? Trigger { get; set; }
14 |
15 | /// Initializes a new instance of the class.
16 | public WebHookFilter() { }
17 | /// Initializes a new instance of the class.
18 | public WebHookFilter(IWebHookFilter filter)
19 | {
20 | if (filter == null)
21 | {
22 | throw new ArgumentNullException(nameof(filter));
23 | }
24 |
25 | Id = filter.Id;
26 | Trigger = filter.Trigger;
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/Harpoon.Common/Sender/DefaultSignatureService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography;
3 | using System.Text;
4 |
5 | namespace Harpoon.Sender
6 | {
7 | ///
8 | /// A class able to assign a signature to content using a secret
9 | ///
10 | public class DefaultSignatureService : ISignatureService
11 | {
12 | ///
13 | /// The provider secret is not a 64 characters string
14 | public string GetSignature(string secret, string content)
15 | {
16 | if (secret?.Length != 64)
17 | {
18 | throw new ArgumentException("Secret needs to be a 64 characters string.");
19 | }
20 |
21 | var secretBytes = Encoding.UTF8.GetBytes(secret);
22 | var data = Encoding.UTF8.GetBytes(content ?? "");
23 |
24 | using (var hasher = new HMACSHA256(secretBytes))
25 | {
26 | return Convert.ToBase64String(hasher.ComputeHash(data));
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Harpoon.Common/WebHookWorkItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon
4 | {
5 | ///
6 | /// Unique work item generated by a
7 | ///
8 | public class WebHookWorkItem : IWebHookWorkItem
9 | {
10 | ///
11 | public Guid Id { get; }
12 | ///
13 | public DateTime Timestamp { get; }
14 | ///
15 | public IWebHookNotification Notification { get; }
16 | ///
17 | public IWebHook WebHook { get; }
18 |
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | public WebHookWorkItem(Guid id, IWebHookNotification notification, IWebHook webHook)
23 | {
24 | Notification = notification ?? throw new ArgumentNullException(nameof(notification));
25 | WebHook = webHook ?? throw new ArgumentNullException(nameof(webHook));
26 | Timestamp = DateTime.UtcNow;
27 | Id = id;
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/Harpoon.Common/IWebHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Harpoon
5 | {
6 | ///
7 | /// Represents a webhook, i.e. a subscription to certain internal events that need to translate to url callbacks
8 | ///
9 | public interface IWebHook
10 | {
11 | ///
12 | /// Gets or sets the unique identifier.
13 | ///
14 | Guid Id { get; set; }
15 |
16 | ///
17 | /// Gets the url to be called in case an event matching the is triggered
18 | ///
19 | Uri? Callback { get; }
20 |
21 | ///
22 | /// Gets or sets the shared secret. The secret must be 64 character in default implementation.
23 | ///
24 | string Secret { get; set; }
25 |
26 | ///
27 | /// Gets a value indicating if the given is currently paused.
28 | ///
29 | bool IsPaused { get; }
30 |
31 | ///
32 | /// Gets a collection of describing what should trigger the given
33 | ///
34 | IReadOnlyCollection Filters { get; }
35 | }
36 | }
--------------------------------------------------------------------------------
/Harpoon.MassTransit/Harpoon.MassTransit.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | latest
6 | enable
7 | 0.1.5
8 | 0.1.5
9 | Romain V
10 | Harpoon provides support for sending your own WebHooks. This package allows the queuing via MassTransit of notifications and webHookWorkItems.
11 | Unlicense
12 | https://raw.githubusercontent.com/Poltuu/Harpoon/master/icon.png
13 | webhook
14 | https://github.com/Poltuu/Harpoon
15 | true
16 |
17 |
18 |
19 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/Harpoon.Registrations.EFStorage.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | latest
6 | enable
7 | 0.1.5
8 | 0.1.5
9 | Romain V
10 | Harpoon provides support for sending your own WebHooks. This package contains the default for registration with EF Core.
11 | Unlicense
12 | https://raw.githubusercontent.com/Poltuu/Harpoon/master/icon.png
13 | webhook
14 | https://github.com/Poltuu/Harpoon
15 | true
16 |
17 |
18 |
19 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Harpoon.Common/Harpoon.Common.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | Harpoon
6 | latest
7 | enable
8 | 0.1.5
9 | 0.1.5
10 | Romain V
11 | Harpoon provides support for sending your own WebHooks. This package contains the base classes.
12 | Unlicense
13 | https://raw.githubusercontent.com/Poltuu/Harpoon/master/icon.png
14 | webhook
15 | true
16 |
17 |
18 |
19 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Harpoon.Registrations/Harpoon.Registrations.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | latest
6 | enable
7 | 0.1.5
8 | 0.1.5
9 | Romain V
10 | Harpoon provides support for sending your own WebHooks. This package contains the default for registration.
11 | Unlicense
12 | https://raw.githubusercontent.com/Poltuu/Harpoon/master/icon.png
13 | webhook
14 | https://github.com/Poltuu/Harpoon
15 | true
16 |
17 |
18 |
19 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Harpoon.MassTransit/PublisherService.cs:
--------------------------------------------------------------------------------
1 | using MassTransit;
2 | using System;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace Harpoon.MassTransit
7 | {
8 | ///
9 | /// This class provides an implementation of and via event publishing
10 | ///
11 | public class PublisherService : IWebHookService, IWebHookSender
12 | {
13 | private readonly IPublishEndpoint _publishEndpoint;
14 |
15 | /// Initializes a new instance of the class.
16 | public PublisherService(IPublishEndpoint publishEndpoint)
17 | {
18 | _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint));
19 | }
20 |
21 | ///
22 | Task IWebHookService.NotifyAsync(IWebHookNotification notification, CancellationToken cancellationToken)
23 | => PublishAsync(notification, cancellationToken);
24 |
25 | ///
26 | Task IWebHookSender.SendAsync(IWebHookWorkItem webHookWorkItem, CancellationToken cancellationToken)
27 | => PublishAsync(webHookWorkItem, cancellationToken);
28 |
29 | private Task PublishAsync(T message, CancellationToken token) => _publishEndpoint.Publish(message, token);
30 | }
31 | }
--------------------------------------------------------------------------------
/Harpoon.Controllers/WebHookTriggersController.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Registrations;
2 | using Microsoft.AspNetCore.Authorization;
3 | using Microsoft.AspNetCore.Mvc;
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | namespace Harpoon.Controllers
8 | {
9 | ///
10 | /// allows the caller to get the list of triggers available for registration.
11 | ///
12 | [Authorize, ApiController, Route("api/webhooktriggers"), ApiExplorerSettings(GroupName = OpenApi.GroupName), Produces("application/json")]
13 | public class WebHookTriggersController : ControllerBase
14 | {
15 | private readonly IWebHookTriggerProvider _webHookTriggerProvider;
16 |
17 | /// Initializes a new instance of the class.
18 | public WebHookTriggersController(IWebHookTriggerProvider webHookTriggerProvider)
19 | {
20 | _webHookTriggerProvider = webHookTriggerProvider ?? throw new ArgumentNullException(nameof(webHookTriggerProvider));
21 | }
22 |
23 | ///
24 | /// Returns available WebHookTriggers for WebHook registration.
25 | ///
26 | ///
27 | [HttpGet]
28 | public ActionResult> Get()
29 | => Ok(_webHookTriggerProvider.GetAvailableTriggers().Values);
30 | }
31 | }
--------------------------------------------------------------------------------
/Harpoon.Controllers/Harpoon.Controllers.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | latest
6 | enable
7 | 0.1.5
8 | 0.1.5
9 | Romain V
10 | Harpoon provides support for sending your own WebHooks. This package contains default web controllers.
11 | Unlicense
12 | https://raw.githubusercontent.com/Poltuu/Harpoon/master/icon.png
13 | webhook
14 | https://github.com/Poltuu/Harpoon
15 | true
16 |
17 |
18 |
19 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/WebHookNotification.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Harpoon.Registrations.EFStorage
5 | {
6 | ///
7 | /// Represents the content of an event that triggered
8 | ///
9 | public class WebHookNotification : IWebHookNotification
10 | {
11 | ///
12 | /// Gets or sets the id.
13 | ///
14 | public Guid Id { get; set; }
15 |
16 | ///
17 | /// Gets or sets the time stamp when the notification was created
18 | ///
19 | public DateTime CreatedAt { get; set; }
20 |
21 | ///
22 | public string TriggerId { get; set; }
23 |
24 | ///
25 | public object Payload { get; set; }
26 |
27 | ///
28 | /// Gets or sets the number of applicable webhooks
29 | ///
30 | public int Count { get; set; }
31 |
32 | ///
33 | /// Gets or sets the associated collection of
34 | ///
35 | public List WebHookLogs { get; set; }
36 |
37 | /// Initializes a new instance of the class.
38 | public WebHookNotification()
39 | {
40 | Id = Guid.NewGuid();
41 | WebHookLogs = new List();
42 | CreatedAt = DateTime.UtcNow;
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/Harpoon.Controllers/Models/WebHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace Harpoon.Controllers.Models
6 | {
7 | ///
8 | /// Represents a webhook, i.e. a subscription to certain internal events that need to translate to url callbacks
9 | ///
10 | public class WebHook : IWebHook
11 | {
12 | ///
13 | public Guid Id { get; set; }
14 | ///
15 | public Uri? Callback { get; set; }
16 | ///
17 | public string? Secret { get; set; }
18 | ///
19 | public bool IsPaused { get; set; }
20 |
21 | ///
22 | public List Filters { get; set; } = new List();
23 |
24 | IReadOnlyCollection IWebHook.Filters => Filters;
25 |
26 | /// Initializes a new instance of the class.
27 | public WebHook() { }
28 | /// Initializes a new instance of the class.
29 | public WebHook(IWebHook webHook)
30 | {
31 | if (webHook == null)
32 | {
33 | throw new ArgumentNullException(nameof(webHook));
34 | }
35 |
36 | Id = webHook.Id;
37 | Callback = webHook.Callback;
38 | Secret = webHook.Secret;
39 | IsPaused = webHook.IsPaused;
40 |
41 | Filters = webHook.Filters.Select(f => new WebHookFilter(f)).ToList();
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/Fixtures/BackgroundSenderFixture.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Background;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Hosting;
5 | using Microsoft.Extensions.Logging;
6 | using Moq;
7 | using System;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 |
11 | namespace Harpoon.Tests.Fixtures
12 | {
13 | public class BackgroundSenderFixture : IDisposable
14 | {
15 | public class FakeWebHookSender : IQueuedProcessor
16 | {
17 | public Task ProcessAsync(IWebHookWorkItem workItem, CancellationToken token)
18 | {
19 | FakeWebHookSenderCount++;
20 | return Task.CompletedTask;
21 | }
22 | }
23 |
24 | public static int FakeWebHookSenderCount { get; private set; }
25 |
26 | private readonly IHost _host;
27 | public IServiceProvider Services => _host.Services;
28 |
29 | public BackgroundSenderFixture()
30 | {
31 | _host = new HostBuilder().ConfigureServices(services =>
32 | {
33 | services.AddSingleton(new Mock>>().Object);
34 | services.AddScoped, FakeWebHookSender>();
35 | services.AddHostedService>();
36 | services.AddSingleton>();
37 | }).Build();
38 | _host.Start();
39 | }
40 |
41 | public void Dispose() => Task.Run(() => _host.StopAsync());
42 | }
43 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/WebHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Harpoon.Registrations.EFStorage
5 | {
6 | ///
7 | /// Default implementation
8 | ///
9 | public class WebHook : IWebHook
10 | {
11 | ///
12 | public Guid Id { get; set; }
13 |
14 | ///
15 | Uri IWebHook.Callback => string.IsNullOrEmpty(Callback) ? null : new Uri(Callback);
16 |
17 | ///
18 | public string Callback { get; set; }
19 |
20 | ///
21 | public string Secret { get; set; }
22 |
23 | ///
24 | /// Gets or sets protected secret
25 | ///
26 | public string ProtectedSecret { get; set; }
27 |
28 | ///
29 | public string PrincipalId { get; set; }
30 |
31 | ///
32 | public bool IsPaused { get; set; }
33 |
34 | ///
35 | /// Gets or sets the associated collection of
36 | ///
37 | public List Filters { get; set; }
38 |
39 | ///
40 | /// Gets or sets the associated collection of
41 | ///
42 | public List WebHookLogs { get; set; }
43 |
44 | IReadOnlyCollection IWebHook.Filters => Filters;
45 |
46 | /// Initializes a new instance of the class.
47 | public WebHook()
48 | {
49 | Filters = new List();
50 | WebHookLogs = new List();
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/ServicesCollectionsExtensions.cs:
--------------------------------------------------------------------------------
1 | using Harpoon;
2 | using Harpoon.Registrations;
3 | using Microsoft.Extensions.DependencyInjection.Extensions;
4 | using System;
5 |
6 | namespace Microsoft.Extensions.DependencyInjection
7 | {
8 | ///
9 | /// A set of extensions methods on
10 | ///
11 | public static class ServicesCollectionsExtensions
12 | {
13 | ///
14 | /// Registers services to use the default implementation. Necessary to use default controllers.
15 | ///
16 | ///
17 | ///
18 | public static IHarpoonBuilder UseDefaultValidator(this IHarpoonBuilder harpoon) => harpoon.UseDefaultValidator(b => { });
19 |
20 | ///
21 | /// Registers services to use the default implementation. Necessary to use default controllers.
22 | ///
23 | ///
24 | ///
25 | ///
26 | public static IHarpoonBuilder UseDefaultValidator(this IHarpoonBuilder harpoon, Action validatorPolicy)
27 | {
28 | if (validatorPolicy == null)
29 | {
30 | throw new ArgumentNullException(nameof(validatorPolicy));
31 | }
32 |
33 | harpoon.Services.TryAddScoped();
34 |
35 | var builder = harpoon.Services.AddHttpClient();
36 | validatorPolicy(builder);
37 | return harpoon;
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/Harpoon.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | latest
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | all
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Harpoon.Registrations/WebHookTrigger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon.Registrations
4 | {
5 | ///
6 | /// Represents an strongly-typed event template that may trigger a webhook
7 | ///
8 | public class WebHookTrigger : WebHookTrigger
9 | {
10 | ///
11 | /// Initializes a new instance of the class.
12 | ///
13 | public WebHookTrigger(string id, string description)
14 | : base(id, description, typeof(TPayload))
15 | {
16 | }
17 | }
18 |
19 | ///
20 | /// Represents an event template that may trigger a webhook
21 | ///
22 | public class WebHookTrigger
23 | {
24 | ///
25 | /// Gets or sets a unique id for the event. This could typically look like `noun.verb`.
26 | /// If pattern matching is used, this could look like this `noun.verb.{value}`
27 | ///
28 | public string Id { get; private set; }
29 |
30 | ///
31 | /// Gets or sets a short description of the event
32 | ///
33 | public string Description { get; set; }
34 |
35 | private readonly Type _payloadType;
36 |
37 | ///
38 | /// Initializes a new instance of the class.
39 | ///
40 | public WebHookTrigger(string id, string description, Type payloadType)
41 | {
42 | Id = id;
43 | Description = description;
44 | _payloadType = payloadType;
45 | }
46 |
47 | ///
48 | /// Gets the payload type for this trigger
49 | ///
50 | public Type GetPayloadType() => _payloadType;
51 | }
52 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/DefaultSecretProtector.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.DataProtection;
2 | using System;
3 | using System.Text;
4 |
5 | namespace Harpoon.Registrations.EFStorage
6 | {
7 | ///
8 | /// Default class able to protect and unprotect a piece of plain text data
9 | ///
10 | public class DefaultSecretProtector : ISecretProtector
11 | {
12 | ///
13 | /// Gets the default purpose
14 | ///
15 | public const string Purpose = "WebHookStorage";
16 |
17 | private readonly IDataProtector _dataProtector;
18 |
19 | /// Initializes a new instance of the class.
20 | ///
21 | public DefaultSecretProtector(IDataProtectionProvider dataProtectionProvider)
22 | {
23 | _dataProtector = dataProtectionProvider?.CreateProtector(Purpose) ?? throw new ArgumentNullException(nameof(dataProtectionProvider));
24 | }
25 |
26 | ///
27 | public string Protect(string plaintext) => _dataProtector.Protect(plaintext);
28 |
29 | ///
30 | public string Unprotect(string protectedData)
31 | {
32 | try
33 | {
34 | return _dataProtector.Unprotect(protectedData);
35 | }
36 | catch
37 | {
38 | if (!(_dataProtector is IPersistedDataProtector persistedProtector))
39 | {
40 | throw;
41 | }
42 |
43 | return Encoding.UTF8.GetString(persistedProtector.DangerousUnprotect(Encoding.UTF8.GetBytes(protectedData), true, out var _, out var _));
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/WebHookLog.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Harpoon.Registrations.EFStorage
4 | {
5 | ///
6 | /// Represents a log for a callback call
7 | ///
8 | public class WebHookLog
9 | {
10 | ///
11 | /// Gets or sets the id.
12 | ///
13 | public int Id { get; set; }
14 |
15 | ///
16 | /// Gets or sets the time stamp when the treatment started
17 | ///
18 | public DateTime CreatedAt { get; set; }
19 |
20 | ///
21 | /// Gets or sets an error message
22 | ///
23 | public string? Error { get; set; }
24 |
25 | ///
26 | /// Gets a value representing if the log is in error
27 | ///
28 | public bool IsSuccess => string.IsNullOrEmpty(Error);
29 |
30 | ///
31 | /// Gets or sets the associated id
32 | ///
33 | public Guid WebHookNotificationId { get; set; }
34 | ///
35 | /// Gets or sets the associated
36 | ///
37 | public WebHookNotification WebHookNotification { get; set; }
38 |
39 | ///
40 | /// Gets or sets the associated id
41 | ///
42 | public Guid WebHookId { get; set; }
43 | ///
44 | /// Gets or sets the associated
45 | ///
46 | public WebHook WebHook { get; set; }
47 |
48 | /// Initializes a new instance of the class.
49 | public WebHookLog()
50 | {
51 | CreatedAt = DateTime.UtcNow;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/EFNotificationProcessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Harpoon.Registrations.EFStorage;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace Harpoon.Sender.EF
10 | {
11 | ///
12 | /// implementation that logs everything into the context
13 | ///
14 | ///
15 | public class EFNotificationProcessor : DefaultNotificationProcessor
16 | where TContext : DbContext, IRegistrationsContext
17 | {
18 | private readonly TContext _context;
19 |
20 | ///
21 | /// Initializes a new instance of the class.
22 | ///
23 | ///
24 | ///
25 | ///
26 | ///
27 | public EFNotificationProcessor(TContext context, IWebHookStore webHookStore, IWebHookSender webHookSender, ILogger logger)
28 | : base(webHookStore, webHookSender, logger)
29 | {
30 | _context = context ?? throw new ArgumentNullException(nameof(context));
31 | }
32 |
33 | ///
34 | protected override async Task LogAsync(IWebHookNotification notification, IReadOnlyList webHooks, CancellationToken cancellationToken)
35 | {
36 | var notif = new Registrations.EFStorage.WebHookNotification
37 | {
38 | Payload = notification.Payload,
39 | TriggerId = notification.TriggerId,
40 | Count = webHooks.Count
41 | };
42 | _context.Add(notif);
43 |
44 | await _context.SaveChangesAsync();
45 | return notif.Id;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/BackgroundSenderTests.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Background;
2 | using Harpoon.Registrations.EFStorage;
3 | using Harpoon.Tests.Fixtures;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using System;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Xunit;
9 |
10 | namespace Harpoon.Tests
11 | {
12 | public class BackgroundSenderTests : IClassFixture
13 | {
14 | private readonly BackgroundSenderFixture _fixture;
15 |
16 | public BackgroundSenderTests(BackgroundSenderFixture fixture)
17 | {
18 | _fixture = fixture;
19 | }
20 |
21 | [Fact]
22 | public async Task ArgNullAsync()
23 | {
24 | Assert.Throws(() => new BackgroundSender(null));
25 | var service = new BackgroundSender(new BackgroundQueue());
26 | await Assert.ThrowsAsync(() => service.SendAsync(null, CancellationToken.None));
27 | }
28 |
29 | [Fact]
30 | public async Task NormalAsync()
31 | {
32 | var queue = new BackgroundQueue();
33 | var service = new BackgroundSender(queue);
34 | var count = 0;
35 |
36 | await service.SendAsync(new WebHookWorkItem(Guid.NewGuid(), new WebHookNotification("", new object()), new WebHook()), CancellationToken.None);
37 | await queue.DequeueAsync(CancellationToken.None).AsTask().ContinueWith(t => count++);
38 |
39 | Assert.Equal(1, count);
40 | }
41 |
42 | [Fact]
43 | public async Task NormalWithHostedServiceAsync()
44 | {
45 | var service = new BackgroundSender(_fixture.Services.GetRequiredService>());
46 | await service.SendAsync(new WebHookWorkItem(Guid.NewGuid(), new WebHookNotification("", new object()), new WebHook()), CancellationToken.None);
47 |
48 | await Task.Delay(10000);
49 | Assert.Equal(1, BackgroundSenderFixture.FakeWebHookSenderCount);
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations/DefaultPrincipalIdGetter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Security.Claims;
4 | using System.Security.Principal;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace Harpoon.Registrations
9 | {
10 | ///
11 | /// Default class able to extract a string from a principal, that will be later used as an id for webhook registration
12 | ///
13 | public class DefaultPrincipalIdGetter : IPrincipalIdGetter
14 | {
15 | ///
16 | /// Current principal id could not be found
17 | public Task GetPrincipalIdAsync(IPrincipal principal, CancellationToken cancellationToken = default)
18 | => Task.FromResult(GetPrincipalIdForWebHookRegistration(principal));
19 |
20 | private string GetPrincipalIdForWebHookRegistration(IPrincipal principal)
21 | {
22 | if (principal == null)
23 | {
24 | throw new ArgumentNullException(nameof(principal));
25 | }
26 |
27 | if (principal is ClaimsPrincipal claimsPrincipal)
28 | {
29 | if (TryGetNotNullClaimValue(claimsPrincipal, ClaimTypes.Name, out var name))
30 | {
31 | return name;
32 | }
33 |
34 | if (TryGetNotNullClaimValue(claimsPrincipal, ClaimTypes.NameIdentifier, out var nameIdentifier))
35 | {
36 | return nameIdentifier;
37 | }
38 | }
39 |
40 | if (principal.Identity?.Name != null)
41 | {
42 | return principal.Identity.Name;
43 | }
44 |
45 | throw new ArgumentException("Current principal id could not be found.");
46 | }
47 |
48 | private bool TryGetNotNullClaimValue(ClaimsPrincipal principal, string claimType, [NotNullWhen(true)] out string? result)
49 | {
50 | result = principal.FindFirst(claimType)?.Value;
51 | return result != null;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/Harpoon.Common/Background/QueuedHostedService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.Logging;
4 | using System;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace Harpoon.Background
9 | {
10 | internal class QueuedHostedService : BackgroundService
11 | {
12 | private readonly IServiceProvider _services;
13 | private readonly ILogger> _logger;
14 |
15 | private readonly BackgroundQueue _webHooksQueue;
16 |
17 | public QueuedHostedService(IServiceProvider services, ILogger> logger, BackgroundQueue webHooksQueue)
18 | {
19 | _services = services ?? throw new ArgumentNullException(nameof(services));
20 | _logger = logger ?? throw new ArgumentNullException(nameof(logger));
21 | _webHooksQueue = webHooksQueue ?? throw new ArgumentNullException(nameof(webHooksQueue));
22 | }
23 |
24 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
25 | {
26 | _logger.LogInformation($"Queued Background Service of {typeof(TWorkItem).Name} is starting.");
27 |
28 | while (!stoppingToken.IsCancellationRequested)
29 | {
30 | var workItem = await _webHooksQueue.DequeueAsync(stoppingToken);
31 |
32 | try
33 | {
34 | using (var scope = _services.CreateScope())
35 | {
36 | var service = scope.ServiceProvider.GetRequiredService>();
37 | await service.ProcessAsync(workItem, stoppingToken);
38 | }
39 | }
40 | catch (Exception ex)
41 | {
42 | _logger.LogError(ex, $"Queued Hosted Service of {typeof(TWorkItem).Name} error.");
43 | }
44 | }
45 |
46 | _logger.LogWarning($"Queued Hosted Service of {typeof(TWorkItem).Name} has been canceled by token.");
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/WebHookStoreTests.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Registrations.EFStorage;
2 | using Harpoon.Tests.Mocks;
3 | using Moq;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Threading.Tasks;
7 | using Xunit;
8 |
9 | namespace Harpoon.Tests
10 | {
11 | public class WebHookStoreTests
12 | {
13 | [Fact]
14 | public async Task ArgNull()
15 | {
16 | Assert.Throws(() => new WebHookStore(null, new Mock().Object));
17 | Assert.Throws(() => new WebHookStore(new InMemoryContext(), null));
18 |
19 | var store = new WebHookStore(new InMemoryContext(), new Mock().Object);
20 |
21 | await Assert.ThrowsAsync(() => store.GetApplicableWebHooksAsync(null));
22 | }
23 |
24 | [Fact]
25 | public async Task TriggerMatchingTests()
26 | {
27 | var notification = new WebHookNotification("something.interesting.happened", new object());
28 |
29 | var context = new InMemoryContext();
30 | context.Add(new WebHook
31 | {
32 | IsPaused = false,
33 | Filters = new List { new WebHookFilter { Trigger = "something.interesting.happened" } }
34 | });
35 | context.Add(new WebHook
36 | {
37 | IsPaused = true,
38 | Filters = new List { new WebHookFilter { Trigger = "something.interesting.happened" } }
39 | });
40 | context.SaveChanges();
41 |
42 | var secret = "http://www.example.org";
43 | var protector = new Mock();
44 | protector.Setup(p => p.Unprotect(It.IsAny())).Returns(secret);
45 |
46 | var store = new WebHookStore(context, protector.Object);
47 |
48 | var result = await store.GetApplicableWebHooksAsync(notification);
49 | Assert.Equal(1, result.Count);
50 |
51 | var webhook = result[0];
52 | Assert.False(webhook.IsPaused);
53 |
54 | Assert.Equal(secret, webhook.Secret);
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/Fixtures/DatabaseFixture.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Registrations;
2 | using Harpoon.Registrations.EFStorage;
3 | using Harpoon.Tests.Mocks;
4 | using Microsoft.AspNetCore.DataProtection;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Logging;
7 | using Moq;
8 | using System;
9 | using System.Security.Principal;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 | namespace Harpoon.Tests.Fixtures
14 | {
15 | public class DatabaseFixture : IDisposable
16 | {
17 | private IServiceProvider _provider;
18 |
19 | public IServiceProvider Provider => _provider ??= GetProvider();
20 |
21 | private IServiceProvider GetProvider()
22 | {
23 | var services = new ServiceCollection();
24 | services.AddEntityFrameworkSqlServer().AddDbContext();
25 |
26 | var getter = new Mock();
27 | getter.Setup(g => g.GetPrincipalIdAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult("principal1"));
28 | services.AddSingleton(getter.Object);
29 |
30 | var protector = new Mock();
31 | protector.Setup(p => p.Protect(It.IsAny())).Returns(v => v);
32 | protector.Setup(p => p.Unprotect(It.IsAny())).Returns(v => v);
33 | protector.Setup(s => s.CreateProtector(It.IsAny())).Returns(protector.Object);
34 | services.AddSingleton(protector.Object);
35 | services.AddSingleton();
36 |
37 | services.AddSingleton(new Mock>>().Object);
38 | services.AddSingleton>();
39 | services.AddSingleton>();
40 |
41 | var result = services.BuildServiceProvider();
42 |
43 | result.GetRequiredService().Database.EnsureCreated();
44 |
45 | return result;
46 | }
47 |
48 | public void Dispose()
49 | {
50 | if (_provider != null)
51 | {
52 | var context = _provider.GetRequiredService();
53 | context.Database.EnsureDeleted();
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/Harpoon.Registrations.EFStorage/WebHookStore.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace Harpoon.Registrations.EFStorage
9 | {
10 | ///
11 | /// Default implementation using EF
12 | ///
13 | ///
14 | public class WebHookStore : IWebHookStore
15 | where TContext : DbContext, IRegistrationsContext
16 | {
17 | private readonly TContext _context;
18 | private readonly ISecretProtector _secretProtector;
19 |
20 | /// Initializes a new instance of the class.
21 | public WebHookStore(TContext context, ISecretProtector secretProtector)
22 | {
23 | _context = context ?? throw new ArgumentNullException(nameof(context));
24 | _secretProtector = secretProtector ?? throw new ArgumentNullException(nameof(secretProtector));
25 | }
26 |
27 | ///
28 | public async Task> GetApplicableWebHooksAsync(IWebHookNotification notification, CancellationToken cancellationToken = default)
29 | {
30 | if (notification == null)
31 | {
32 | throw new ArgumentNullException(nameof(notification));
33 | }
34 |
35 | var webHooks = await FilterQuery(_context.WebHooks.AsNoTracking()
36 | .Where(w => !w.IsPaused)
37 | .Include(w => w.Filters), notification)
38 | .ToListAsync(cancellationToken);
39 |
40 | foreach (var webHook in webHooks)
41 | {
42 | webHook.Secret = _secretProtector.Unprotect(webHook.ProtectedSecret);
43 | }
44 | return webHooks;
45 | }
46 |
47 | ///
48 | /// Apply the SQL filter matching the current notification
49 | ///
50 | ///
51 | ///
52 | ///
53 | protected virtual IQueryable FilterQuery(IQueryable query, IWebHookNotification notification)
54 | => query.Where(w => w.Filters.Count == 0 || w.Filters.Any(f => f.Trigger == notification.TriggerId));
55 | }
56 | }
--------------------------------------------------------------------------------
/Harpoon.Tests/DefaultPrincipalIdGetterTests.cs:
--------------------------------------------------------------------------------
1 | using Harpoon.Registrations;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Security.Claims;
5 | using System.Threading.Tasks;
6 | using Xunit;
7 |
8 | namespace Harpoon.Tests
9 | {
10 | public class DefaultPrincipalIdGetterTests
11 | {
12 | [Fact]
13 | public async Task ArgNull()
14 | {
15 | var service = new DefaultPrincipalIdGetter();
16 |
17 | await Assert.ThrowsAsync(() => service.GetPrincipalIdAsync(null));
18 | }
19 |
20 | public static IEnumerable