├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── ci ├── install-redis-server.ps1 └── set-version-from-assemblyinformationalversion.ps1 ├── docs └── images │ ├── Webhooks.Architecture.PNG │ ├── Webhooks.Default.PNG │ └── Webhooks.pptx └── src ├── .nuget ├── NuGet.Config ├── NuGet.exe └── NuGet.targets ├── Directory.Build.props ├── GlobalAssemblyInfo.cs ├── ServiceStack.Webhooks.DotSettings ├── ServiceStack.Webhooks.sln ├── ServiceStack.Webhooks.sln.DotSettings ├── ServiceStack.Webhooks.snk ├── ServiceStack.Webhooks.v3.ncrunchsolution ├── UnitTesting.Common ├── MoqExtensions.cs ├── NUintExtensions.cs ├── Properties │ └── AssemblyInfo.cs ├── UnitTesting.Common.csproj └── UnitTesting.Common.v3.ncrunchproject ├── Webhooks.Common ├── DateTimeExtensions.cs ├── Guard.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── ServiceInterface │ ├── AnonymousCurrentCaller.cs │ ├── AuthSessionCurrentCaller.cs │ ├── CurrentCallerExtensions.cs │ ├── ICurrentCaller.cs │ └── RequestExtensions.cs ├── StringExtensions.cs ├── Webhooks.Common.csproj └── Webhooks.Common.v3.ncrunchproject ├── Webhooks.IntTests ├── CacheClientSubscriptionStoreSpec.cs ├── MemorySubscriptionStoreSpec.cs ├── Properties │ └── AssemblyInfo.cs ├── Services │ ├── AppHostForTesting.cs │ ├── StubSubscriberService.cs │ └── TestService.cs ├── SubscriberSpec.cs ├── SubscriptionServiceSpec.cs ├── SubscriptionStoreSpecBase.cs ├── Webhooks.IntTests.csproj └── Webhooks.IntTests.v3.ncrunchproject ├── Webhooks.Interfaces.UnitTests ├── Properties │ └── AssemblyInfo.cs ├── Security │ └── HmacUtilsSpec.cs ├── Webhooks.Interfaces.UnitTests.csproj └── Webhooks.Interfaces.UnitTests.v3.ncrunchproject ├── Webhooks.Interfaces ├── Clients │ └── IEventServiceClient.cs ├── DataFormats.cs ├── IEventPublisher.cs ├── IEventRelay.cs ├── IEventSink.cs ├── IEventSubscriptionCache.cs ├── ISubscriptionStore.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Security │ └── HmacUtils.cs ├── ServiceModel │ ├── CreateSubscription.cs │ ├── DeleteSubscription.cs │ ├── GetSubscription.cs │ ├── ListSubscriptions.cs │ ├── SearchSubscriptions.cs │ ├── Subscription.cs │ ├── Types │ │ ├── SubscriptionDeliveryResult.cs │ │ ├── SubscriptionRelayConfig.cs │ │ └── WebhookSubscription.cs │ ├── UpdateSubscription.cs │ └── UpdateSubscriptionHistory.cs ├── WebHookExtensions.cs ├── WebHookInterfaces.cs ├── WebhookEvent.cs ├── WebhookEventConstants.cs ├── Webhooks.Interfaces.csproj └── Webhooks.Interfaces.v3.ncrunchproject ├── Webhooks.OrmLite.IntTests ├── OrmLiteSubscriptionStoreSpec.cs ├── Properties │ └── AssemblyInfo.cs ├── Webhooks.OrmLite.IntTests.csproj └── Webhooks.OrmLite.IntTests.v3.ncrunchproject ├── Webhooks.OrmLite ├── OrmLiteSubscriptionStore.cs ├── Properties │ └── AssemblyInfo.cs ├── Webhooks.OrmLite.csproj └── Webhooks.OrmLite.v3.ncrunchproject ├── Webhooks.Relays.UnitTests ├── CacheClientEventSubscriptionCacheSpec.cs ├── Clients │ └── EventServiceClientSpec.cs ├── Properties │ └── AssemblyInfo.cs ├── Webhooks.Relays.UnitTests.csproj └── Webhooks.Relays.UnitTests.v3.ncrunchproject ├── Webhooks.Relays ├── CacheClientEventSubscriptionCache.cs ├── Clients │ ├── EventServiceClient.cs │ ├── EventServiceClientFactory.cs │ ├── IEventServiceClientFactory.cs │ ├── IServiceClient.cs │ └── ServiceClient.cs ├── ISubscriptionService.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Webhooks.Relays.csproj └── Webhooks.Relays.v3.ncrunchproject ├── Webhooks.Subscribers.UnitTests ├── Properties │ └── AssemblyInfo.cs ├── Security │ └── HmacAuthProviderSpec.cs ├── Webhooks.Subscribers.UnitTests.csproj └── Webhooks.Subscribers.UnitTests.v3.ncrunchproject ├── Webhooks.Subscribers ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── Security │ └── HmacAuthProvider.cs ├── Webhooks.Subscribers.csproj └── Webhooks.Subscribers.v3.ncrunchproject ├── Webhooks.UnitTests ├── AppHostEventSinkSpec.cs ├── CacheClientSubscriptionStoreSpec.cs ├── MemorySubscriptionStoreSpec.cs ├── Properties │ └── AssemblyInfo.cs ├── ServiceInterface │ └── SubscriptionServiceSpec.cs ├── ServiceModel │ ├── CreateSubscriptionValidatorSpec.cs │ ├── DeleteSubscriptionValidatorSpec.cs │ ├── GetSubscriptionValidatorSpec.cs │ ├── ListSubscriptionsValidatorSpec.cs │ ├── SearchSubscriptionsValidatorSpec.cs │ ├── SubscriptionConfigValidatorSpec.cs │ ├── SubscriptionDeliveryResultValidatorSpec.cs │ ├── SubscriptionEventsValidatorSpec.cs │ ├── UpdateSubscriptionHistoryValidatorSpec.cs │ └── UpdateSubscriptionValidatorSpec.cs ├── WebhookFeatureSpec.cs ├── Webhooks.UnitTests.csproj ├── Webhooks.UnitTests.v3.ncrunchproject └── WebhooksClientSpec.cs └── Webhooks ├── AppHostEventSink.cs ├── AsyncHelper.cs ├── CacheClientSubscriptionStore.cs ├── IWebhooks.cs ├── MemorySubscriptionStore.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx ├── ServiceInterface └── SubscriptionService.cs ├── ServiceModel ├── CreateSubscription.cs ├── DeleteSubscription.cs ├── EntityIdValidator.cs ├── GetSubscription.cs ├── ListSubscriptions.cs ├── SearchSubscriptions.cs ├── UpdateSubscription.cs ├── UpdateSubscriptionHistory.cs ├── UrlValidator.cs └── ValidatorExtensions.cs ├── WebhookFeature.cs ├── Webhooks.csproj ├── Webhooks.v3.ncrunchproject └── WebhooksClient.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | .vs 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.sln.docstates 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Rr]elease/ 20 | x64/ 21 | *_i.c 22 | *_p.c 23 | *.ilk 24 | *.meta 25 | *.obj 26 | *.pch 27 | *.pdb 28 | *.pgc 29 | *.pgd 30 | *.rsp 31 | *.sbr 32 | *.tlb 33 | *.tli 34 | *.tlh 35 | *.tmp 36 | *.log 37 | *.vspscc 38 | *.vssscc 39 | .builds 40 | 41 | # Visual C++ cache files 42 | ipch/ 43 | *.aps 44 | *.ncb 45 | *.opensdf 46 | *.sdf 47 | 48 | # Visual Studio profiler 49 | *.psess 50 | *.vsp 51 | *.vspx 52 | 53 | # ReSharper is a .NET coding add-in 54 | _ReSharper* 55 | 56 | # NCrunch 57 | *.ncrunch* 58 | .*crunch*.local.xml 59 | !*.ncrunchsolution 60 | !*.ncrunchproject 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | Downloaded Profiling Logs 86 | csx 87 | def 88 | rcf 89 | *.build.csdef 90 | 91 | # Windows Store app package directory 92 | AppPackages/ 93 | 94 | # Others 95 | [Bb]in 96 | [Oo]bj 97 | sql 98 | TestResults 99 | [Tt]est[Rr]esult* 100 | *.Cache 101 | ClientBin 102 | [Ss]tyle[Cc]op.* 103 | ~$* 104 | *.dbmdl 105 | Generated_Code #added for RIA/Silverlight projects 106 | 107 | # Backup & report files from converting an old project file to a newer 108 | # Visual Studio version. Backup files are not needed, because we have git ;-) 109 | _UpgradeReport_Files/ 110 | Backup*/ 111 | UpgradeLog*.XML 112 | /src/Toolkit/Binaries 113 | 114 | # Database Files 115 | *.mdf 116 | *.ldf 117 | 118 | # ReSharper 119 | _ReSharper*/ 120 | 121 | # Nuget 122 | *.nupkg 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Build status](https://ci.appveyor.com/api/projects/status/j2a8skqibee6d7vt/branch/master?svg=true)](https://ci.appveyor.com/project/JezzSantos/servicestack-webhooks/branch/master) 2 | 3 | 4 | [![NuGet](https://img.shields.io/nuget/v/ServiceStack.Webhooks.svg?label=ServiceStack.Webhooks)](https://www.nuget.org/packages/ServiceStack.Webhooks) [![NuGet](https://img.shields.io/nuget/v/ServiceStack.Webhooks.Ormlite.svg?label=ServiceStack.Webhooks.Ormlite)](https://www.nuget.org/packages/ServiceStack.Webhooks.Ormlite) 5 | 6 | # Add Webhooks to your ServiceStack services 7 | 8 | [![Release Notes](https://img.shields.io/nuget/v/ServiceStack.Webhooks.svg?label=Release%20Notes&colorB=green)](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki/Release-Notes) 9 | 10 | # Overview 11 | 12 | This project makes it very easy to expose webhook notifications from your ServiceStack services, and helps you manage your user's subscriptions to those webhooks. 13 | 14 | By adding the `WebhookFeature` to the `AppHost` of your service, you automatically get all the pieces you need to raise and manage the events raised by your services. 15 | 16 | We _know_ that most services are built for scalability and to be hosted in the cloud, so we know that you are going to want to use your own components and technologies that fit in with your own architecture. All you have to do is plug into the `WebhookFeature`. 17 | 18 | **For example:** 19 | In one service, you may want to store the Webhook subscriptions in a _MongoDB database_, and have an _Azure worker role_ relay the events to subscribers from a (reliable) cloud _queue_. 20 | 21 | In another service, you may want to store subscriptions in _Ormlite SQL database_, and relay events to subscribers directly from within the same service _on a background thread_, or throw the event to an _AWS lambda_ to process. Whatever works for you, the choice is yours. 22 | 23 | _Oh, don't worry, getting started is easy. We got your back with a built-in subscription store and built-in event sink that will get you going seeing how it all works. But eventually you'll want to swap those out for your own pieces that fit your architecture, which is dead easy._ 24 | 25 | ![](https://raw.githubusercontent.com/jezzsantos/ServiceStack.Webhooks/master/docs/images/Webhooks.Architecture.PNG) 26 | 27 | If you cant find the component you want for your architecture (see [Plugins](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki/Plugins)), it should be easy for you to build add your own and _just plug it in_. 28 | 29 | # [Getting Started](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki/Getting-Started) 30 | 31 | Install from NuGet: 32 | ``` 33 | Install-Package ServiceStack.Webhooks 34 | ``` 35 | 36 | Simply add the `WebhookFeature` in your `AppHost.Configure()` method: 37 | 38 | ``` 39 | public override void Configure(Container container) 40 | { 41 | // Add ValidationFeature and AuthFeature plugins first 42 | 43 | Plugins.Add(new WebhookFeature()); 44 | } 45 | ``` 46 | 47 | See [Getting Started](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki/Getting-Started) for more details. 48 | 49 | ## Raising Events 50 | 51 | To raise events from your own services: 52 | 53 | 1. Add the `IWebhooks` dependency to your service 54 | 2. Call: `IWebhooks.Publish(string eventName, TDto data)` 55 | 56 | As simple as this: 57 | 58 | ``` 59 | internal class HelloService : Service 60 | { 61 | public IWebhooks Webhooks { get; set; } 62 | 63 | public HelloResponse Any(Hello request) 64 | { 65 | Webhooks.Publish("hello", new HelloEvent{ Text = "I said hello" }); 66 | } 67 | } 68 | ``` 69 | 70 | ## Subscribing to Events 71 | 72 | Subscribers to events raised by your services need to create a webhook subscription to those events. 73 | 74 | They do this by POSTing something like the following, to your service: 75 | 76 | ``` 77 | POST /webhooks/subscriptions 78 | { 79 | "name": "My Webhook", 80 | "events": ["hello", "goodbye"], 81 | "config": { 82 | "url": "http://myserver/api/incoming", 83 | } 84 | } 85 | ``` 86 | 87 | ## Consuming Events 88 | 89 | To consume events, a subscriber needs to provide a public HTTP POST endpoint on the internet that would receive the POSTed webhook event. 90 | 91 | The URL to that endpoint is defined in the `config.url` of the subscription (above). 92 | 93 | In the case of the "hello" event (raised above), the POSTed event sent to the subscriber's endpoint might look something like this: 94 | 95 | ``` 96 | POST http://myserver/hello HTTP/1.1 97 | Accept: application/json 98 | User-Agent: ServiceStack .NET Client 4.56 99 | Accept-Encoding: gzip,deflate 100 | X-Webhook-Delivery: 7a6224aad9c8400fb0a70b8a71262400 101 | X-Webhook-Event: hello 102 | Content-Type: application/json 103 | Host: myserver 104 | Content-Length: 26 105 | Expect: 100-continue 106 | Proxy-Connection: Keep-Alive 107 | 108 | { 109 | "Text": "I said hello" 110 | } 111 | ``` 112 | 113 | To consume this event with a ServiceStack service, the subscriber would standup a public API like the one below, that could receive the 'Hello' event. That might have been raised from another service with a call to `Webhooks.Publish("hello", new HelloEvent{ Text = "I said hello" })`: 114 | 115 | ``` 116 | internal class MyService : Service 117 | { 118 | public void Post(HelloDto request) 119 | { 120 | // They said hello! 121 | var message = request.Text; 122 | 123 | 124 | // The event name, messaging metadata are included in the headers 125 | var eventName = Request.Headers["X-Webhook-Event"]; 126 | var deliveryId = Request.Headers["X-Webhook-Delivery"]; 127 | var signature = Request.Headers["X-Hub-Signature"]; 128 | } 129 | } 130 | 131 | [Route("/hello", "POST")] 132 | public class HelloDto 133 | { 134 | public string Text { get; set; } 135 | } 136 | ``` 137 | 138 | Note: Webhook events can be delivered securely to subscribers using signatures, that proves the authenticity of the sender only. Delivered events are never encrypted, and only signed. See [Subscriber Security](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki/Subscriber-Security) for more details. 139 | 140 | # [Documentation](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki) 141 | 142 | More documentation about how the `WebhookFeature` works, and how to customize it are available in [here](https://github.com/jezzsantos/ServiceStack.Webhooks/wiki) 143 | 144 | ### Contribute? 145 | 146 | Want to get involved in this project? or want to help improve this capability for your services? just send us a message or pull-request! 147 | 148 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 3.0.{build} 2 | image: Visual Studio 2017 3 | environment: 4 | matrix: 5 | - SERVICESTACK_LICENSE: 6 | secure: 5zYTw+1lLlf6ydh05spg4huc/S2VUb+Ka5b3etCB8bvVQTAlMabBx7mcTxadOnCJo4EwPPc94TyDFEhJ031XcSds4I6NL3TEVzcY1y73tibAYaejOgWs5kNzcIjLbemn/SqHw7bvrmUSJVs2M9UFYkqyv/GaDO7O/j/VrZSZOB6KwW4QHanV7RDZpSnepoQ5fPXatIbimF3KXGTQGMHcffTBjpBsvfzOOc05fXtuvfLlv8eG3fqOvN7IUlyC9EOCBgDr6JjJFKmm9CbPNChkaLnyieMvWf2GRUSZaOjV2MmBvdSmDsIrNW26rUS+K4V5+NQ2gGWkxJApOMsg6+QvVf7UGR4npVkgkelYSW1QMVfTWxNCbLd4bEi8PRkCgxLSM07VWvNrFBPUZWkEQtNCaBHhhvbtsBOKr0CpEPJKFO4L+p7AG6wDhPED7vk3g81p 7 | buildconfig: Release 8 | buildfrx: net472 9 | - SERVICESTACK_LICENSE: 10 | secure: 5zYTw+1lLlf6ydh05spg4huc/S2VUb+Ka5b3etCB8bvVQTAlMabBx7mcTxadOnCJo4EwPPc94TyDFEhJ031XcSds4I6NL3TEVzcY1y73tibAYaejOgWs5kNzcIjLbemn/SqHw7bvrmUSJVs2M9UFYkqyv/GaDO7O/j/VrZSZOB6KwW4QHanV7RDZpSnepoQ5fPXatIbimF3KXGTQGMHcffTBjpBsvfzOOc05fXtuvfLlv8eG3fqOvN7IUlyC9EOCBgDr6JjJFKmm9CbPNChkaLnyieMvWf2GRUSZaOjV2MmBvdSmDsIrNW26rUS+K4V5+NQ2gGWkxJApOMsg6+QvVf7UGR4npVkgkelYSW1QMVfTWxNCbLd4bEi8PRkCgxLSM07VWvNrFBPUZWkEQtNCaBHhhvbtsBOKr0CpEPJKFO4L+p7AG6wDhPED7vk3g81p 11 | buildconfig: ReleaseNoTestDeploy 12 | buildfrx: net472 13 | build_script: 14 | - cmd: >- 15 | cd src 16 | 17 | cd .nuget 18 | 19 | appveyor-retry nuget.exe restore ..\ServiceStack.Webhooks.sln -DisableParallelProcessing 20 | 21 | cd .. 22 | 23 | msbuild.exe ServiceStack.Webhooks.sln /t:Rebuild /p:Configuration=%buildconfig% /verbosity:minimal 24 | 25 | cd.. 26 | test_script: 27 | - cmd: >- 28 | nunit3-console "C:\projects\servicestack-webhooks\src\Webhooks.Interfaces.UnitTests\bin\%buildconfig%\%buildfrx%\ServiceStack.Webhooks.Interfaces.UnitTests.dll" "C:\projects\servicestack-webhooks\src\Webhooks.Relays.UnitTests\bin\%buildconfig%\%buildfrx%\ServiceStack.Webhooks.Relays.UnitTests.dll" "C:\projects\servicestack-webhooks\src\Webhooks.Subscribers.UnitTests\bin\%buildconfig%\%buildfrx%\ServiceStack.Webhooks.Subscribers.UnitTests.dll" "C:\projects\servicestack-webhooks\src\Webhooks.UnitTests\bin\%buildconfig%\%buildfrx%\ServiceStack.Webhooks.UnitTests.dll" --result=myresults.xml;format=AppVeyor 29 | 30 | 31 | nunit3-console "C:\projects\servicestack-webhooks\src\Webhooks.IntTests\bin\%buildconfig%\%buildfrx%\ServiceStack.Webhooks.IntTests.dll" "C:\projects\servicestack-webhooks\src\Webhooks.OrmLite.IntTests\bin\%buildconfig%\%buildfrx%\ServiceStack.Webhooks.OrmLite.IntTests.dll" --inprocess --result=myresults.xml;format=AppVeyor 32 | artifacts: 33 | - path: '**\ServiceStack.Webhooks.*.nupkg' 34 | deploy: 35 | - provider: NuGet 36 | api_key: 37 | secure: IOhQyRtNmDLFQCHDAlihYB9gVTqimDBKBPEcjSvJEwRF7Hlkxncz3a+FEWsKCSJv 38 | skip_symbols: true 39 | on: 40 | branch: master -------------------------------------------------------------------------------- /ci/install-redis-server.ps1: -------------------------------------------------------------------------------- 1 | nuget install redis-64 -excludeversion 2 | redis-64\tools\redis-server.exe --service-install 3 | redis-64\tools\redis-server.exe --service-start 4 | -------------------------------------------------------------------------------- /ci/set-version-from-assemblyinformationalversion.ps1: -------------------------------------------------------------------------------- 1 | $assemblyFile = "$env:APPVEYOR_BUILD_FOLDER\src\GlobalAssemblyInfo.cs" 2 | $regex = new-object System.Text.RegularExpressions.Regex ('(AssemblyInformationalVersion(Attribute)?\s*\(\s*\")(.*)(\"\s*\))', 3 | [System.Text.RegularExpressions.RegexOptions]::MultiLine) 4 | $content = [IO.File]::ReadAllText($assemblyFile) 5 | $version = $null 6 | $match = $regex.Match($content) 7 | if($match.Success) { 8 | $version = $match.groups[3].value 9 | } 10 | 11 | $version = "$version.$env:APPVEYOR_BUILD_NUMBER" 12 | Update-AppveyorBuild -Version $version -------------------------------------------------------------------------------- /docs/images/Webhooks.Architecture.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezzsantos/ServiceStack.Webhooks/69d812e4a5bd85aa0ec990710b10748d6435193b/docs/images/Webhooks.Architecture.PNG -------------------------------------------------------------------------------- /docs/images/Webhooks.Default.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezzsantos/ServiceStack.Webhooks/69d812e4a5bd85aa0ec990710b10748d6435193b/docs/images/Webhooks.Default.PNG -------------------------------------------------------------------------------- /docs/images/Webhooks.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezzsantos/ServiceStack.Webhooks/69d812e4a5bd85aa0ec990710b10748d6435193b/docs/images/Webhooks.pptx -------------------------------------------------------------------------------- /src/.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezzsantos/ServiceStack.Webhooks/69d812e4a5bd85aa0ec990710b10748d6435193b/src/.nuget/NuGet.exe -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | $([System.IO.File]::ReadAllText('$(SolutionDir)GlobalAssemblyInfo.cs')) 10 | $([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs')) 11 | ^\s*\[assembly: AssemblyDescription\(\s*"([^"]+)" 12 | $([System.Text.RegularExpressions.Regex]::Match($(Info), $(Pattern), System.Text.RegularExpressions.RegexOptions.Multiline).Groups[1].Value) 13 | ^\s*\[assembly: AssemblyCompany\(\s*"([^"]+)" 14 | $([System.Text.RegularExpressions.Regex]::Match($(GlobalInfo), $(Pattern), System.Text.RegularExpressions.RegexOptions.Multiline).Groups[1].Value) 15 | ^\s*\[assembly: AssemblyCopyright\(\s*"([^"]+)" 16 | $([System.Text.RegularExpressions.Regex]::Match($(GlobalInfo), $(Pattern), System.Text.RegularExpressions.RegexOptions.Multiline).Groups[1].Value) 17 | ^\s*\[assembly: AssemblyInformationalVersion\(\s*"([^"]+)" 18 | $([System.Text.RegularExpressions.Regex]::Match($(GlobalInfo), $(Pattern), System.Text.RegularExpressions.RegexOptions.Multiline).Groups[1].Value) 19 | 20 | 21 | https://github.com/jezzsantos/ServiceStack.Webhooks/blob/master/LICENSE 22 | https://github.com/jezzsantos/ServiceStack.Webhooks 23 | https://github.com/jezzsantos/ServiceStack.Webhooks 24 | GitHub 25 | https://github.com/jezzsantos/ServiceStack.Webhooks/blob/master/README.md 26 | false 27 | 28 | 29 | 30 | 31 | true 32 | 33 | 34 | ..\ServiceStack.Webhooks.snk 35 | false 36 | false 37 | 436 38 | 39 | 40 | 41 | 42 | true 43 | full 44 | false 45 | bin\Debug\ 46 | DEBUG;TRACE;ASSEMBLYSIGNED;TESTINGONLY 47 | DEBUG;TRACE;TESTINGONLY 48 | prompt 49 | 4 50 | false 51 | 52 | 53 | 54 | pdbonly 55 | true 56 | bin\Release\ 57 | TRACE;ASSEMBLYSIGNED;TESTINGONLY 58 | TRACE;TESTINGONLY 59 | prompt 60 | 4 61 | false 62 | 63 | 64 | 65 | pdbonly 66 | true 67 | bin\ReleaseNoTestDeploy\ 68 | TRACE;ASSEMBLYSIGNED 69 | TRACE 70 | prompt 71 | 4 72 | true 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/GlobalAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyConfiguration("")] 6 | [assembly: AssemblyCompany("jezzsantos")] 7 | [assembly: AssemblyProduct("ServiceStack.Webhooks")] 8 | [assembly: AssemblyCopyright("Copyright jezzsantos © 2020")] 9 | [assembly: AssemblyTrademark("")] 10 | [assembly: AssemblyCulture("")] 11 | [assembly: ComVisible(false)] 12 | [assembly: AssemblyVersion("3.0.0.0")] 13 | [assembly: AssemblyFileVersion("3.1.0.0")] 14 | [assembly: AssemblyInformationalVersion("3.1.0")] 15 | 16 | #if ASSEMBLYSIGNED 17 | [assembly: 18 | InternalsVisibleTo( 19 | "DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" 20 | )] 21 | #else 22 | 23 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 24 | #endif -------------------------------------------------------------------------------- /src/ServiceStack.Webhooks.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | C:\Projects\github\jezzsantos\ServiceStack.Webhooks\src\ServiceStack.Webhooks.DotSettings 3 | ..\ServiceStack.Webhooks.DotSettings 4 | True 5 | True 6 | 1 -------------------------------------------------------------------------------- /src/ServiceStack.Webhooks.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jezzsantos/ServiceStack.Webhooks/69d812e4a5bd85aa0ec990710b10748d6435193b/src/ServiceStack.Webhooks.snk -------------------------------------------------------------------------------- /src/ServiceStack.Webhooks.v3.ncrunchsolution: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | True 5 | 6 | -------------------------------------------------------------------------------- /src/UnitTesting.Common/MoqExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Moq; 6 | using Moq.Language; 7 | using Moq.Language.Flow; 8 | 9 | namespace ServiceStack.Webhooks.UnitTesting 10 | { 11 | /// 12 | /// Extensions to the class. 13 | /// 14 | public static class MoqExtensions 15 | { 16 | /// 17 | /// Setup an expectation to return a sequence of values 18 | /// 19 | /// 20 | /// Credit to: http://haacked.com/archive/2009/09/29/moq-sequences.aspx/ 21 | /// 22 | public static void ReturnsInOrder(this ISetup setup, 23 | params TResult[] results) where T : class 24 | { 25 | setup.Returns(new Queue(results).Dequeue); 26 | } 27 | 28 | /// 29 | /// Setup an expectation to return a sequence of values 30 | /// 31 | /// 32 | /// Credit to: http://haacked.com/archive/2010/11/24/moq-sequences-revisited.aspx/ 33 | /// 34 | public static void ReturnsInOrder(this ISetup setup, 35 | params object[] results) where T : class 36 | { 37 | var queue = new Queue(results); 38 | setup.Returns(() => 39 | { 40 | var result = queue.Dequeue(); 41 | if (result is Exception) 42 | { 43 | throw result as Exception; 44 | } 45 | return (TResult) result; 46 | }); 47 | } 48 | 49 | /// 50 | /// Throws the specified exception for the specified 51 | /// 52 | public static IReturnsResult ThrowsAsync(this IReturns mock, Exception exception) where TMock : class 53 | { 54 | var completionSource = new TaskCompletionSource(); 55 | completionSource.SetException(exception); 56 | return mock.Returns(completionSource.Task); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/UnitTesting.Common/NUintExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using NUnit.Framework; 3 | using NUnit.Framework.Constraints; 4 | 5 | namespace ServiceStack.Webhooks.UnitTesting 6 | { 7 | public static class ThrowsWebServiceException 8 | { 9 | public static EqualConstraint WithStatusCode(HttpStatusCode status) 10 | { 11 | return Throws.InstanceOf().With.Property(@"StatusCode").EqualTo((int) status); 12 | } 13 | } 14 | 15 | public static class ThrowsHttpError 16 | { 17 | public static EqualConstraint WithStatusCode(HttpStatusCode status) 18 | { 19 | return Throws.InstanceOf().With.Property(@"StatusCode").EqualTo(status); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/UnitTesting.Common/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.UnitTesting.Common")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("6afe38a5-2660-436e-84b0-0a83091fd0e6")] -------------------------------------------------------------------------------- /src/UnitTesting.Common/UnitTesting.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.UnitTesting 9 | ServiceStack.Webhooks.UnitTesting.Common 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 5.9.0 19 | 20 | 21 | 4.12.0 22 | 23 | 24 | 3.12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/UnitTesting.Common/UnitTesting.Common.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | False 4 | True 5 | 6 | -------------------------------------------------------------------------------- /src/Webhooks.Common/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Common")] 5 | [assembly: AssemblyDescription("Common types for working with webhooks")] 6 | [assembly: Guid("ebd7353a-7bcd-4b65-90b9-fdc9a6dca7fe")] -------------------------------------------------------------------------------- /src/Webhooks.Common/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ServiceStack.Webhooks.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ServiceStack.Webhooks.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Parameter cannot be empty.. 65 | /// 66 | internal static string Guard_ArgumentCanNotBeEmpty { 67 | get { 68 | return ResourceManager.GetString("Guard_ArgumentCanNotBeEmpty", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Parameter cannot be null.. 74 | /// 75 | internal static string Guard_ArgumentCanNotBeNull { 76 | get { 77 | return ResourceManager.GetString("Guard_ArgumentCanNotBeNull", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Webhooks.Common/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Parameter cannot be empty. 122 | 123 | 124 | Parameter cannot be null. 125 | 126 | -------------------------------------------------------------------------------- /src/Webhooks.Common/ServiceInterface/AnonymousCurrentCaller.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ServiceStack.Webhooks.ServiceInterface 5 | { 6 | public class AnonymousCurrentCaller : ICurrentCaller 7 | { 8 | public string AccessToken 9 | { 10 | get { return null; } 11 | } 12 | 13 | public string Username 14 | { 15 | get { return null; } 16 | } 17 | 18 | public string UserId 19 | { 20 | get { return null; } 21 | } 22 | 23 | public IEnumerable Roles 24 | { 25 | get { return Enumerable.Empty(); } 26 | } 27 | 28 | public bool IsAuthenticated 29 | { 30 | get { return false; } 31 | } 32 | 33 | public bool IsInRole(string role) 34 | { 35 | return false; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Webhooks.Common/ServiceInterface/AuthSessionCurrentCaller.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using ServiceStack.Auth; 4 | using ServiceStack.Web; 5 | 6 | namespace ServiceStack.Webhooks.ServiceInterface 7 | { 8 | public class AuthSessionCurrentCaller : ICurrentCaller, IRequiresRequest 9 | { 10 | private IRequest currentRequest; 11 | private IAuthSession currentSession; 12 | 13 | public string Username 14 | { 15 | get 16 | { 17 | var sess = GetSession(); 18 | if (sess == null) 19 | { 20 | return null; 21 | } 22 | 23 | return sess.UserName; 24 | } 25 | } 26 | 27 | public string UserId 28 | { 29 | get 30 | { 31 | var sess = GetSession(); 32 | if (sess == null) 33 | { 34 | return null; 35 | } 36 | 37 | return sess.UserAuthId; 38 | } 39 | } 40 | 41 | public IEnumerable Roles 42 | { 43 | get 44 | { 45 | var sess = GetSession(); 46 | if (sess == null) 47 | { 48 | return Enumerable.Empty(); 49 | } 50 | 51 | return sess.Roles.Safe(); 52 | } 53 | } 54 | 55 | public bool IsAuthenticated 56 | { 57 | get 58 | { 59 | var sess = GetSession(); 60 | if (sess == null) 61 | { 62 | return false; 63 | } 64 | 65 | return sess.IsAuthenticated; 66 | } 67 | } 68 | 69 | public bool IsInRole(string role) 70 | { 71 | return Roles.Any(r => r.EqualsIgnoreCase(role)); 72 | } 73 | 74 | public IRequest Request 75 | { 76 | get { return currentRequest; } 77 | set 78 | { 79 | currentRequest = value; 80 | currentSession = null; 81 | } 82 | } 83 | 84 | private IAuthSession GetSession() 85 | { 86 | if (currentRequest == null) 87 | { 88 | return null; 89 | } 90 | 91 | if (currentSession == null) 92 | { 93 | currentSession = currentRequest.GetSession(); 94 | } 95 | 96 | return currentSession; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/Webhooks.Common/ServiceInterface/CurrentCallerExtensions.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Web; 2 | 3 | namespace ServiceStack.Webhooks.ServiceInterface 4 | { 5 | public static class CurrentCallerExtensions 6 | { 7 | public static void InjectRequestIfRequires(this ICurrentCaller caller, IRequest request) 8 | { 9 | Guard.AgainstNull(() => caller, caller); 10 | Guard.AgainstNull(() => request, request); 11 | 12 | var requiresRequest = caller as IRequiresRequest; 13 | if (requiresRequest != null) 14 | { 15 | requiresRequest.Request = request; 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Webhooks.Common/ServiceInterface/ICurrentCaller.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ServiceStack.Webhooks.ServiceInterface 4 | { 5 | /// 6 | /// Defines the calling user 7 | /// 8 | public interface ICurrentCaller 9 | { 10 | /// 11 | /// Gets the name of the user 12 | /// 13 | string Username { get; } 14 | 15 | /// 16 | /// Gets the identifier of the user 17 | /// 18 | string UserId { get; } 19 | 20 | /// 21 | /// Gets the roles of the user 22 | /// 23 | IEnumerable Roles { get; } 24 | 25 | /// 26 | /// Gets whether the user is authenticated 27 | /// 28 | bool IsAuthenticated { get; } 29 | 30 | /// 31 | /// Determines whether the user is in the specified role 32 | /// 33 | bool IsInRole(string role); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Webhooks.Common/ServiceInterface/RequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Web; 2 | 3 | namespace ServiceStack.Webhooks.ServiceInterface 4 | { 5 | public static class RequestExtensions 6 | { 7 | /// 8 | /// Returns an instance of the registered in the IOC container 9 | /// 10 | public static ICurrentCaller ToCaller(this IRequest request) 11 | { 12 | Guard.AgainstNull(() => request, request); 13 | 14 | var caller = request.TryResolve(); 15 | if (caller == null) 16 | { 17 | return new AnonymousCurrentCaller(); 18 | } 19 | 20 | var requiresrequest = caller as IRequiresRequest; 21 | if (requiresrequest != null) 22 | { 23 | caller.InjectRequestIfRequires(request); 24 | } 25 | 26 | return caller; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Webhooks.Common/Webhooks.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard20;net472 5 | Library 6 | 7 | ServiceStack.Webhooks 8 | ServiceStack.Webhooks.Common 9 | Debug;Release;ReleaseNoTestDeploy 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | True 20 | True 21 | Resources.resx 22 | 23 | 24 | 25 | 26 | ResXFileCodeGenerator 27 | Resources.Designer.cs 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Webhooks.Common/Webhooks.Common.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.IntTests/CacheClientSubscriptionStoreSpec.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Caching; 2 | 3 | namespace ServiceStack.Webhooks.IntTests 4 | { 5 | public class CacheClientSubscriptionStoreSpec 6 | { 7 | public class GivenCacheClientSubscriptionStoreAndNoUser : GivenNoUserWithSubscriptionStoreBase 8 | { 9 | public override ISubscriptionStore GetSubscriptionStore() 10 | { 11 | return new CacheClientSubscriptionStore 12 | { 13 | CacheClient = new MemoryCacheClient() 14 | }; 15 | } 16 | } 17 | 18 | public class GivenCacheClientSubscriptionStoreAndAUser : GivenAUserWithSubscriptionStoreBase 19 | { 20 | public override ISubscriptionStore GetSubscriptionStore() 21 | { 22 | return new CacheClientSubscriptionStore 23 | { 24 | CacheClient = new MemoryCacheClient() 25 | }; 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Webhooks.IntTests/MemorySubscriptionStoreSpec.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks.IntTests 2 | { 3 | public class MemorySubscriptionStoreSpec 4 | { 5 | public class GivenMemorySubscriptionStoreAndNoUser : GivenNoUserWithSubscriptionStoreBase 6 | { 7 | public override ISubscriptionStore GetSubscriptionStore() 8 | { 9 | return new MemorySubscriptionStore(); 10 | } 11 | } 12 | 13 | public class GivenMemorySubscriptionStoreAndAUser : GivenAUserWithSubscriptionStoreBase 14 | { 15 | public override ISubscriptionStore GetSubscriptionStore() 16 | { 17 | return new MemorySubscriptionStore(); 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Webhooks.IntTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.IntTests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("eafbb9ba-792b-4ecd-9728-45f35b5291b2")] -------------------------------------------------------------------------------- /src/Webhooks.IntTests/Services/AppHostForTesting.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Funq; 3 | using ServiceStack.Auth; 4 | using ServiceStack.Validation; 5 | using ServiceStack.Webhooks.Subscribers.Security; 6 | 7 | namespace ServiceStack.Webhooks.IntTests.Services 8 | { 9 | public class AppHostForTesting : AppSelfHostBase 10 | { 11 | public AppHostForTesting() : base("AppHostForTesting", typeof(AppHostForTesting).Assembly) 12 | { 13 | } 14 | 15 | public override void Configure(Container container) 16 | { 17 | Config.DebugMode = true; 18 | Config.ReturnsInnerException = true; 19 | RegisterAuthentication(container); 20 | Plugins.Add(new ValidationFeature()); 21 | Plugins.Add(new WebhookFeature()); 22 | 23 | // We need this filter to allow us to read the request body in IRequest.GetRawBody() 24 | PreRequestFilters.Insert(0, (httpReq, httpRes) => { httpReq.UseBufferedStream = true; }); 25 | } 26 | 27 | private void RegisterAuthentication(Container container) 28 | { 29 | Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] 30 | { 31 | new BasicAuthProvider(), 32 | new CredentialsAuthProvider(), 33 | new HmacAuthProvider 34 | { 35 | RequireSecureConnection = false, 36 | Secret = StubSubscriberService.SubscriberSecret 37 | } 38 | })); 39 | Plugins.Add(new RegistrationFeature()); 40 | container.Register(new InMemoryAuthRepository()); 41 | } 42 | 43 | public string LoginUser(JsonServiceClient client, string username, string roles) 44 | { 45 | var password = "apassword"; 46 | var userRepo = Resolve(); 47 | if (userRepo.GetUserAuthByUserName(username) == null) 48 | { 49 | string hash; 50 | string salt; 51 | new SaltedHash().GetHashAndSaltString(password, out hash, out salt); 52 | 53 | userRepo.CreateUserAuth(new UserAuth 54 | { 55 | UserName = username, 56 | Roles = roles.SafeSplit(WebhookFeature.RoleDelimiters).ToList(), 57 | PasswordHash = hash, 58 | Salt = salt 59 | }, password); 60 | } 61 | 62 | return client.Post(new Authenticate 63 | { 64 | UserName = username, 65 | Password = password, 66 | provider = CredentialsAuthProvider.Name 67 | }).UserId; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Webhooks.IntTests/Services/StubSubscriberService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.Security; 3 | using ServiceStack.Webhooks.Subscribers.Security; 4 | 5 | namespace ServiceStack.Webhooks.IntTests.Services 6 | { 7 | internal class StubSubscriberService : Service 8 | { 9 | public const string SubscriberSecret = "asignaturesecret"; 10 | 11 | private static readonly List Events = new List(); 12 | 13 | [Route("/consume", "POST")] 14 | public void Post(ConsumeEvent request) 15 | { 16 | ConsumeEvent(request); 17 | } 18 | 19 | [Authenticate(HmacAuthProvider.Name), Route("/consume/secured", "POST")] 20 | public void Post(ConsumeEventWithAuth request) 21 | { 22 | ConsumeEvent(request); 23 | } 24 | 25 | private void ConsumeEvent(ConsumeEvent request) 26 | { 27 | var isValidSignature = false; 28 | var incomingSignature = Request.Headers[WebhookEventConstants.SecretSignatureHeaderName]; 29 | if (incomingSignature != null) 30 | { 31 | isValidSignature = Request.VerifySignature(incomingSignature, SubscriberSecret); 32 | } 33 | 34 | Events.Add(new ConsumedEvent 35 | { 36 | EventName = Request.Headers[WebhookEventConstants.EventNameHeaderName], 37 | Signature = incomingSignature, 38 | IsAuthenticated = isValidSignature, 39 | Data = request.ConvertTo() 40 | }); 41 | } 42 | 43 | public GetConsumedEventsResponse Get(GetConsumedEvents request) 44 | { 45 | return new GetConsumedEventsResponse 46 | { 47 | Events = Events 48 | }; 49 | } 50 | 51 | public void Put(ResetConsumedEvents request) 52 | { 53 | Events.Clear(); 54 | } 55 | } 56 | 57 | [Route("/consumed/reset", "PUT")] 58 | public class ResetConsumedEvents : IReturnVoid 59 | { 60 | } 61 | 62 | [Route("/consumed", "GET")] 63 | public class GetConsumedEvents : IReturn 64 | { 65 | } 66 | 67 | public class GetConsumedEventsResponse 68 | { 69 | public List Events { get; set; } 70 | 71 | public ResponseStatus ResponseStatus { get; set; } 72 | } 73 | 74 | public class ConsumeEvent : IReturnVoid 75 | { 76 | public object A { get; set; } 77 | 78 | public object B { get; set; } 79 | 80 | public ConsumedNestedObject C { get; set; } 81 | } 82 | 83 | public class ConsumeEventWithAuth : ConsumeEvent 84 | { 85 | } 86 | 87 | public class ConsumedNestedObject 88 | { 89 | public object D { get; set; } 90 | 91 | public object E { get; set; } 92 | 93 | public object F { get; set; } 94 | } 95 | 96 | public class ConsumedEvent 97 | { 98 | public string EventName { get; set; } 99 | 100 | public TestEvent Data { get; set; } 101 | 102 | public string Signature { get; set; } 103 | 104 | public bool IsAuthenticated { get; set; } 105 | } 106 | } -------------------------------------------------------------------------------- /src/Webhooks.IntTests/Services/TestService.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks.IntTests.Services 2 | { 3 | internal class TestService : Service 4 | { 5 | public IWebhooks Webhooks { get; set; } 6 | 7 | public void Any(RaiseEvent request) 8 | { 9 | Webhooks.Publish(request.EventName, request.Data); 10 | } 11 | } 12 | 13 | [Route("/raise")] 14 | public class RaiseEvent : IReturnVoid 15 | { 16 | public string EventName { get; set; } 17 | 18 | public TestEvent Data { get; set; } 19 | } 20 | 21 | public class TestEvent 22 | { 23 | public object A { get; set; } 24 | 25 | public object B { get; set; } 26 | 27 | public TestNestedEvent C { get; set; } 28 | } 29 | 30 | public class TestNestedEvent 31 | { 32 | public object D { get; set; } 33 | 34 | public object E { get; set; } 35 | 36 | public object F { get; set; } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Webhooks.IntTests/Webhooks.IntTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.IntTests 9 | ServiceStack.Webhooks.IntTests 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Webhooks.IntTests/Webhooks.IntTests.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | True 5 | 6 | -------------------------------------------------------------------------------- /src/Webhooks.Interfaces.UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Interfaces.UnitTests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("d19c5f17-efe3-43d8-bb34-3237792707ac")] -------------------------------------------------------------------------------- /src/Webhooks.Interfaces.UnitTests/Security/HmacUtilsSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Moq; 4 | using NUnit.Framework; 5 | using ServiceStack.Web; 6 | using ServiceStack.Webhooks.Security; 7 | 8 | namespace ServiceStack.Webhooks.Interfaces.UnitTests.Security 9 | { 10 | public class HmacUtilsSpec 11 | { 12 | [TestFixture] 13 | public class GivenAContext 14 | { 15 | private string body; 16 | private Mock request; 17 | 18 | [SetUp] 19 | public void Initialize() 20 | { 21 | body = "abody"; 22 | request = new Mock(); 23 | request.Setup(req => req.GetRawBody()) 24 | .Returns(body); 25 | } 26 | 27 | [Test, Category("Unit")] 28 | public void WhenCreateHmacSignatureWithNullRequestBytes_ThenThrows() 29 | { 30 | Assert.Throws(() => 31 | ((byte[]) null).CreateHmacSignature("asecret")); 32 | } 33 | 34 | [Test, Category("Unit")] 35 | public void WhenCreateHmacSignatureWithNullSecret_ThenThrows() 36 | { 37 | Assert.Throws(() => 38 | new byte[] {}.CreateHmacSignature(null)); 39 | } 40 | 41 | [Test, Category("Unit")] 42 | public void WhenCreateHmacSignatureWithEmptyBytes_ThenReturnsSignature() 43 | { 44 | var result = new byte[] {}.CreateHmacSignature("asecret"); 45 | 46 | Assert.That(result, Does.Match("[sha1\\=]".Fmt(DataFormats.Base64Text().Expression))); 47 | } 48 | 49 | [Test, Category("Unit")] 50 | public void WhenCreateHmacSignatureWithAnyBytes_ThenReturnsSignature() 51 | { 52 | var result = Encoding.UTF8.GetBytes("abody").CreateHmacSignature("asecret"); 53 | 54 | Assert.That(result, Does.Match("[sha1\\=]".Fmt(DataFormats.Base64Text().Expression))); 55 | } 56 | 57 | [Test, Category("Unit")] 58 | public void WhenVerifySignatureWithNullRequest_ThenThrows() 59 | { 60 | Assert.Throws(() => 61 | ((IRequest) null).VerifySignature("asignature", "asecret")); 62 | } 63 | 64 | [Test, Category("Unit")] 65 | public void WhenVerifySignatureWithNullSignature_ThenThrows() 66 | { 67 | Assert.Throws(() => 68 | request.Object.VerifySignature(null, "asecret")); 69 | } 70 | 71 | [Test, Category("Unit")] 72 | public void WhenVerifySignatureWithNullSecret_ThenThrows() 73 | { 74 | Assert.Throws(() => 75 | request.Object.VerifySignature("asignature", null)); 76 | } 77 | 78 | [Test, Category("Unit")] 79 | public void WhenVerifySignatureWithEmptySignature_ThenReturnsFalse() 80 | { 81 | var result = request.Object.VerifySignature(string.Empty, "asecret"); 82 | 83 | Assert.That(result, Is.False); 84 | } 85 | 86 | [Test, Category("Unit")] 87 | public void WhenVerifySignatureWithWrongSignature_ThenReturnsFalse() 88 | { 89 | var result = request.Object.VerifySignature("awrongsignature", "asecret"); 90 | 91 | Assert.That(result, Is.False); 92 | } 93 | 94 | [Test, Category("Unit")] 95 | public void WhenVerifySignatureWithWrongSecret_ThenReturnsFalse() 96 | { 97 | var signature = Encoding.UTF8.GetBytes(body).CreateHmacSignature("asecret"); 98 | 99 | var result = request.Object.VerifySignature(signature, "awrongsecret"); 100 | 101 | Assert.That(result, Is.False); 102 | } 103 | 104 | [Test, Category("Unit")] 105 | public void WhenVerifySignature_ThenReturnsTrue() 106 | { 107 | var signature = Encoding.UTF8.GetBytes(body).CreateHmacSignature("asecret"); 108 | 109 | var result = request.Object.VerifySignature(signature, "asecret"); 110 | 111 | Assert.That(result, Is.True); 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces.UnitTests/Webhooks.Interfaces.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.Interfaces.UnitTests 9 | ServiceStack.Webhooks.Interfaces.UnitTests 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Webhooks.Interfaces.UnitTests/Webhooks.Interfaces.UnitTests.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Clients/IEventServiceClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.Clients 5 | { 6 | public interface IEventServiceClient 7 | { 8 | /// 9 | /// Gets or sets the timeout for requests 10 | /// 11 | TimeSpan? Timeout { get; set; } 12 | 13 | /// 14 | /// Gets or sets the number of retries a timed out request will be attempted 15 | /// 16 | int Retries { get; set; } 17 | 18 | /// 19 | /// Relays the specified event to the specified subscription configuration 20 | /// 21 | SubscriptionDeliveryResult Relay(SubscriptionRelayConfig subscription, WebhookEvent webhookEvent); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/DataFormats.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ServiceStack.Webhooks 4 | { 5 | public static class DataFormats 6 | { 7 | public static readonly DataFormat EntityIdentifier = 8 | new DataFormat( 9 | @"^[\{]{0,1}[A-Fa-f0-9]{8}[\-]{0,1}[A-Fa-f0-9]{4}[\-]{0,1}[A-Fa-f0-9]{4}[\-]{0,1}[A-Fa-f0-9]{4}[\-]{0,1}[A-Fa-f0-9]{12}[\}]{0,1}$", 10 | 32, 38); 11 | 12 | public static DataFormat DescriptiveName(int min = 1, int max = 100) 13 | { 14 | return 15 | new DataFormat(@"^[\d\w\'\`\#\(\)\-\'\,\.\/ ]{{{0},{1}}}$".Fmt(min, 16 | max), min, max); 17 | } 18 | 19 | public static DataFormat FreeformText(int min = 1, int max = 1000) 20 | { 21 | return 22 | new DataFormat( 23 | @"^[\d\w\`\~\!\@\#\$\%\:\&\*\(\)\-\+\=\:\;\'\""\<\,\>\.\?\/ ]{{{0},{1}}}$".Fmt(min, 24 | max), min, max); 25 | } 26 | 27 | public static DataFormat Base64Text() 28 | { 29 | return 30 | new DataFormat(@"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", 0, 0); 31 | } 32 | 33 | public static string CreateEntityIdentifier() 34 | { 35 | return Guid.NewGuid().ToString("D"); 36 | } 37 | 38 | public static class Subscriptions 39 | { 40 | public static DataFormat Name = FreeformText(4, 100); 41 | public static DataFormat Event = new DataFormat(@"^[\d\w\-_]{4,100}$", 4, 100); 42 | public static DataFormat Secret = Base64Text(); 43 | } 44 | } 45 | 46 | public class DataFormat 47 | { 48 | /// 49 | /// Creates a new instance of the class. 50 | /// 51 | public DataFormat(string expression, int minLength = 0, int maxLength = 0) 52 | { 53 | Expression = expression; 54 | MinLength = minLength; 55 | MaxLength = maxLength; 56 | } 57 | 58 | /// 59 | /// Gets the regular expression 60 | /// 61 | public string Expression { get; private set; } 62 | 63 | /// 64 | /// Gets the maximum string length 65 | /// 66 | public int MaxLength { get; private set; } 67 | 68 | /// 69 | /// Gets the minimum string length 70 | /// 71 | public int MinLength { get; private set; } 72 | } 73 | 74 | public static class DataFormatExtensions 75 | { 76 | public static bool IsEntityId(this string id) 77 | { 78 | return id.IsGuid(); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/IEventPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks 2 | { 3 | public interface IEventPublisher 4 | { 5 | /// 6 | /// Publishes the specified event, with its data 7 | /// 8 | void Publish(string eventName, TDto data); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/IEventRelay.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks 2 | { 3 | public interface IEventRelay 4 | { 5 | void NotifySubscribers(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/IEventSink.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ServiceStack.Webhooks 4 | { 5 | public interface IEventSink 6 | { 7 | /// 8 | /// Writes a new event with data 9 | /// 10 | void Write(WebhookEvent webhookEvent); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/IEventSubscriptionCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks 5 | { 6 | public interface IEventSubscriptionCache 7 | { 8 | /// 9 | /// Gets all the subscriptions for the specified event 10 | /// 11 | List GetAll(string eventName); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ISubscriptionStore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks 5 | { 6 | public interface ISubscriptionStore 7 | { 8 | /// 9 | /// Returns the identity of the added subscription 10 | /// 11 | string Add(WebhookSubscription subscription); 12 | 13 | /// 14 | /// Returns all subscription for the specified userId 15 | /// 16 | List Find(string userId); 17 | 18 | /// 19 | /// Gets the subscription for the specified user and eventName 20 | /// 21 | WebhookSubscription Get(string userId, string eventName); 22 | 23 | /// 24 | /// Gets the specified subscription 25 | /// 26 | WebhookSubscription Get(string subscriptionId); 27 | 28 | /// 29 | /// Updates the specified subscription 30 | /// 31 | void Update(string subscriptionId, WebhookSubscription subscription); 32 | 33 | /// 34 | /// Deletes the specified subscription 35 | /// 36 | void Delete(string subscriptionId); 37 | 38 | /// 39 | /// Returns all subscription configurations for all users for the specified event, 40 | /// and optionally whether they are currently active or not 41 | /// 42 | List Search(string eventName, bool? isActive); 43 | 44 | /// 45 | /// Adds a new delivery result to the subscription 46 | /// 47 | void Add(string subscriptionId, SubscriptionDeliveryResult result); 48 | 49 | /// 50 | /// Returns the top specified delivery results for the specified subscription (in descending date order) 51 | /// 52 | List Search(string subscriptionId, int top); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Interfaces")] 5 | [assembly: AssemblyDescription("Add Webhooks to your ServiceStack services")] 6 | [assembly: Guid("861cb055-15cb-4909-94a4-2a357138d39c")] -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ServiceStack.Webhooks.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ServiceStack.Webhooks.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Security/HmacUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | using ServiceStack.Web; 5 | 6 | namespace ServiceStack.Webhooks.Security 7 | { 8 | public static class HmacUtils 9 | { 10 | internal const string SignatureFormat = @"sha1={0}"; 11 | 12 | /// 13 | /// Returns the computed HMAC hex digest of the body (RFC3174), using the secret as the key. 14 | /// See https://developer.github.com/v3/repos/hooks/#example, and 15 | /// https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#authednotify 16 | /// 17 | public static string CreateHmacSignature(this byte[] requestBytes, string secret) 18 | { 19 | Guard.AgainstNull(() => requestBytes, requestBytes); 20 | Guard.AgainstNullOrEmpty(() => secret, secret); 21 | 22 | var encoding = Encoding.UTF8; 23 | var key = encoding.GetBytes(secret); 24 | var signature = SignatureFormat.Fmt(SignBody(requestBytes, key)); 25 | 26 | return signature; 27 | } 28 | 29 | public static bool VerifySignature(this IRequest request, string signature, string secret) 30 | { 31 | Guard.AgainstNull(() => request, request); 32 | Guard.AgainstNull(() => signature, signature); 33 | Guard.AgainstNullOrEmpty(() => secret, secret); 34 | 35 | var expectedSignature = CreateHmacSignature(request, secret); 36 | 37 | return expectedSignature.EqualsOrdinal(signature); 38 | } 39 | 40 | private static string CreateHmacSignature(IRequest request, string secret) 41 | { 42 | var encoding = Encoding.UTF8; 43 | var body = encoding.GetBytes(request.GetRawBody()); 44 | 45 | var key = encoding.GetBytes(secret); 46 | 47 | var signature = SignatureFormat.Fmt(SignBody(body, key)); 48 | 49 | return signature; 50 | } 51 | 52 | private static string SignBody(byte[] body, byte[] key) 53 | { 54 | var signature = new HMACSHA256(key) 55 | .ComputeHash(body); 56 | 57 | return ToHex(signature); 58 | } 59 | 60 | private static string ToHex(byte[] bytes) 61 | { 62 | var builder = new StringBuilder(); 63 | bytes 64 | .ToList() 65 | .ForEach(b => { builder.Append(b.ToString("x2")); }); 66 | 67 | return builder.ToString(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/CreateSubscription.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | [Route("/webhooks/subscriptions", "POST")] 7 | public class CreateSubscription : IPost, IReturn 8 | { 9 | public string Name { get; set; } 10 | 11 | public List Events { get; set; } 12 | 13 | public SubscriptionConfig Config { get; set; } 14 | } 15 | 16 | public class CreateSubscriptionResponse 17 | { 18 | public ResponseStatus ResponseStatus { get; set; } 19 | 20 | public List Subscriptions { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/DeleteSubscription.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Model; 2 | 3 | namespace ServiceStack.Webhooks.ServiceModel 4 | { 5 | [Route("/webhooks/subscriptions/{Id}", "DELETE")] 6 | public class DeleteSubscription : IDelete, IHasStringId, IReturn 7 | { 8 | public string Id { get; set; } 9 | } 10 | 11 | public class DeleteSubscriptionResponse 12 | { 13 | public ResponseStatus ResponseStatus { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/GetSubscription.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Model; 3 | using ServiceStack.Webhooks.ServiceModel.Types; 4 | 5 | namespace ServiceStack.Webhooks.ServiceModel 6 | { 7 | [Route("/webhooks/subscriptions/{Id}", "GET")] 8 | public class GetSubscription : IGet, IHasStringId, IReturn 9 | { 10 | public string Id { get; set; } 11 | } 12 | 13 | public class GetSubscriptionResponse 14 | { 15 | public ResponseStatus ResponseStatus { get; set; } 16 | 17 | public WebhookSubscription Subscription { get; set; } 18 | 19 | public List History { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/ListSubscriptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | [Route("/webhooks/subscriptions", "GET")] 7 | public class ListSubscriptions : IGet, IReturn 8 | { 9 | } 10 | 11 | public class ListSubscriptionsResponse 12 | { 13 | public ResponseStatus ResponseStatus { get; set; } 14 | 15 | public List Subscriptions { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/SearchSubscriptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | [Route("/webhooks/subscriptions/search", "GET")] 7 | public class SearchSubscriptions : IGet, IReturn 8 | { 9 | public string EventName { get; set; } 10 | } 11 | 12 | public class SearchSubscriptionsResponse 13 | { 14 | public ResponseStatus ResponseStatus { get; set; } 15 | 16 | public List Subscribers { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/Subscription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ServiceStack.Webhooks.ServiceModel 4 | { 5 | public static class Subscription 6 | { 7 | public static Type[] AllSubscriptionDtos = 8 | { 9 | typeof(CreateSubscription), 10 | typeof(DeleteSubscription), 11 | typeof(GetSubscription), 12 | typeof(ListSubscriptions), 13 | typeof(SearchSubscriptions), 14 | typeof(UpdateSubscription), 15 | typeof(UpdateSubscriptionHistory) 16 | }; 17 | } 18 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/Types/SubscriptionDeliveryResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using ServiceStack.Model; 4 | 5 | namespace ServiceStack.Webhooks.ServiceModel.Types 6 | { 7 | public class SubscriptionDeliveryResult : IHasStringId 8 | { 9 | public DateTime AttemptedDateUtc { get; set; } 10 | 11 | public string StatusDescription { get; set; } 12 | 13 | public HttpStatusCode StatusCode { get; set; } 14 | 15 | public string SubscriptionId { get; set; } 16 | 17 | public string Id { get; set; } 18 | 19 | public string EventId { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/Types/SubscriptionRelayConfig.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks.ServiceModel.Types 2 | { 3 | public class SubscriptionRelayConfig 4 | { 5 | public string SubscriptionId { get; set; } 6 | 7 | public SubscriptionConfig Config { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/Types/WebhookSubscription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ServiceStack.Webhooks.ServiceModel.Types 4 | { 5 | public class WebhookSubscription 6 | { 7 | public string Id { get; set; } 8 | 9 | public string Name { get; set; } 10 | 11 | public string Event { get; set; } 12 | 13 | public bool IsActive { get; set; } 14 | 15 | public DateTime CreatedDateUtc { get; set; } 16 | 17 | public string CreatedById { get; set; } 18 | 19 | public DateTime LastModifiedDateUtc { get; set; } 20 | 21 | public SubscriptionConfig Config { get; set; } 22 | } 23 | 24 | public class SubscriptionConfig 25 | { 26 | public string Url { get; set; } 27 | 28 | public string ContentType { get; set; } 29 | 30 | public string Secret { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/UpdateSubscription.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.Model; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | [Route("/webhooks/subscriptions/{Id}", "PUT")] 7 | public class UpdateSubscription : IPut, IHasStringId, IReturn 8 | { 9 | public string Url { get; set; } 10 | 11 | public string Secret { get; set; } 12 | 13 | public string ContentType { get; set; } 14 | 15 | public bool? IsActive { get; set; } 16 | 17 | public string Id { get; set; } 18 | } 19 | 20 | public class UpdateSubscriptionResponse 21 | { 22 | public ResponseStatus ResponseStatus { get; set; } 23 | 24 | public WebhookSubscription Subscription { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/ServiceModel/UpdateSubscriptionHistory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | [Route("/webhooks/subscriptions/history", "PUT")] 7 | public class UpdateSubscriptionHistory : IPut, IReturn 8 | { 9 | public List Results { get; set; } 10 | } 11 | 12 | public class UpdateSubscriptionHistoryResponse 13 | { 14 | public ResponseStatus ResponseStatus { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/WebHookExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks 2 | { 3 | public static class WebHookExtensions 4 | { 5 | /// 6 | /// Create the initial schema for Data Stores that require it 7 | /// 8 | /// 9 | public static void InitSchema(this ISubscriptionStore store) 10 | { 11 | var requiresSchema = store as IRequiresSchema; 12 | if (requiresSchema != null) 13 | { 14 | requiresSchema.InitSchema(); 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/WebHookInterfaces.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks 2 | { 3 | /// 4 | /// Defines a marker class for this assembly 5 | /// 6 | public class WebHookInterfaces 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/WebhookEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ServiceStack.Model; 4 | 5 | namespace ServiceStack.Webhooks 6 | { 7 | public class WebhookEvent : IHasStringId 8 | { 9 | public string EventName { get; set; } 10 | 11 | public DateTime CreatedDateUtc { get; set; } 12 | 13 | public object Data { get; set; } 14 | 15 | public string Id { get; set; } 16 | 17 | public string Origin { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/WebhookEventConstants.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks 2 | { 3 | public static class WebhookEventConstants 4 | { 5 | public const string SecretSignatureHeaderName = @"X-Hub-Signature"; 6 | public const string RequestIdHeaderName = @"X-Webhook-Delivery"; 7 | public const string EventNameHeaderName = @"X-Webhook-Event"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Webhooks.Interfaces.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard20;net472 5 | Library 6 | 7 | ServiceStack.Webhooks 8 | ServiceStack.Webhooks.Interfaces 9 | Debug;Release;ReleaseNoTestDeploy 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | True 20 | True 21 | Resources.resx 22 | 23 | 24 | 25 | 26 | ResXFileCodeGenerator 27 | Resources.Designer.cs 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Webhooks.Interfaces/Webhooks.Interfaces.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.OrmLite.IntTests/OrmLiteSubscriptionStoreSpec.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.OrmLite; 3 | using ServiceStack.Webhooks.IntTests; 4 | 5 | namespace ServiceStack.Webhooks.OrmLite.IntTests 6 | { 7 | public class OrmLiteSubscriptionStoreSpec 8 | { 9 | //For some reason R# wont run any tests if it can't find one 10 | [Test, Category("Integration")] 11 | public void OrmLiteTest() 12 | { 13 | } 14 | 15 | public class GivenOrmLiteSubscriptionStoreAndNoUser : GivenNoUserWithSubscriptionStoreBase 16 | { 17 | public override ISubscriptionStore GetSubscriptionStore() 18 | { 19 | return new OrmLiteSubscriptionStore( 20 | new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider)); 21 | } 22 | } 23 | 24 | public class GivenOrmLiteSubscriptionStoreAndAUser : GivenAUserWithSubscriptionStoreBase 25 | { 26 | public override ISubscriptionStore GetSubscriptionStore() 27 | { 28 | return new OrmLiteSubscriptionStore( 29 | new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider)); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Webhooks.OrmLite.IntTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.OrmLite.IntTests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("faea8fb4-0b9d-4561-902b-5806707a4025")] -------------------------------------------------------------------------------- /src/Webhooks.OrmLite.IntTests/Webhooks.OrmLite.IntTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.OrmLite.IntTests 9 | ServiceStack.Webhooks.OrmLite.IntTests 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Webhooks.OrmLite.IntTests/Webhooks.OrmLite.IntTests.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.OrmLite/OrmLiteSubscriptionStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ServiceStack.Data; 4 | using ServiceStack.OrmLite; 5 | using ServiceStack.Webhooks.ServiceModel.Types; 6 | 7 | namespace ServiceStack.Webhooks.OrmLite 8 | { 9 | public class OrmLiteSubscriptionStore : ISubscriptionStore, IRequiresSchema 10 | { 11 | private readonly IDbConnectionFactory dbFactory; 12 | 13 | public OrmLiteSubscriptionStore(IDbConnectionFactory dbFactory) 14 | { 15 | this.dbFactory = dbFactory; 16 | } 17 | 18 | public void InitSchema() 19 | { 20 | using (var db = dbFactory.Open()) 21 | { 22 | db.CreateTableIfNotExists(); 23 | db.CreateTableIfNotExists(); 24 | } 25 | } 26 | 27 | public string Add(WebhookSubscription subscription) 28 | { 29 | Guard.AgainstNull(() => subscription, subscription); 30 | 31 | var id = DataFormats.CreateEntityIdentifier(); 32 | subscription.Id = id; 33 | 34 | using (var db = dbFactory.Open()) 35 | { 36 | db.Insert(subscription); 37 | } 38 | 39 | return subscription.Id; 40 | } 41 | 42 | public List Find(string userId) 43 | { 44 | using (var db = dbFactory.Open()) 45 | { 46 | return db.Select(x => x.CreatedById == userId); 47 | } 48 | } 49 | 50 | public WebhookSubscription Get(string userId, string eventName) 51 | { 52 | Guard.AgainstNullOrEmpty(() => eventName, eventName); 53 | 54 | using (var db = dbFactory.Open()) 55 | { 56 | return db.Single(x => x.CreatedById == userId && x.Event == eventName); 57 | } 58 | } 59 | 60 | public WebhookSubscription Get(string subscriptionId) 61 | { 62 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 63 | 64 | using (var db = dbFactory.Open()) 65 | { 66 | return db.Single(x => x.Id == subscriptionId); 67 | } 68 | } 69 | 70 | public void Update(string subscriptionId, WebhookSubscription subscription) 71 | { 72 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 73 | Guard.AgainstNull(() => subscription, subscription); 74 | 75 | using (var db = dbFactory.Open()) 76 | { 77 | db.Update(subscription); 78 | } 79 | } 80 | 81 | public void Delete(string subscriptionId) 82 | { 83 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 84 | 85 | using (var db = dbFactory.Open()) 86 | { 87 | db.Delete(x => x.Id == subscriptionId); 88 | } 89 | } 90 | 91 | public List Search(string eventName, bool? isActive) 92 | { 93 | Guard.AgainstNullOrEmpty(() => eventName, eventName); 94 | 95 | using (var db = dbFactory.Open()) 96 | { 97 | var q = db.From() 98 | .Where(x => x.Event == eventName); 99 | 100 | if (isActive != null) 101 | { 102 | q.And(x => x.IsActive == isActive.Value); 103 | } 104 | 105 | return db.Select( 106 | q.Select(x => new {x.Config, SubscriptionId = x.Id})); 107 | } 108 | } 109 | 110 | public void Add(string subscriptionId, SubscriptionDeliveryResult result) 111 | { 112 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 113 | Guard.AgainstNull(() => result, result); 114 | 115 | using (var db = dbFactory.Open()) 116 | { 117 | db.Insert(result); 118 | } 119 | } 120 | 121 | public List Search(string subscriptionId, int top) 122 | { 123 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 124 | if (top <= 0) 125 | { 126 | throw new ArgumentOutOfRangeException(nameof(top)); 127 | } 128 | 129 | using (var db = dbFactory.Open()) 130 | { 131 | return db.Select(db.From() 132 | .Where(x => x.SubscriptionId == subscriptionId) 133 | .OrderByDescending(x => x.AttemptedDateUtc) 134 | .Take(top)); 135 | } 136 | } 137 | 138 | public void Clear() 139 | { 140 | using (var db = dbFactory.Open()) 141 | { 142 | db.DeleteAll(); 143 | db.DeleteAll(); 144 | } 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/Webhooks.OrmLite/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.OrmLite")] 5 | [assembly: AssemblyDescription("Add OrmLite plugins to ServiceStack Webhooks")] 6 | [assembly: Guid("26284e4c-d5bb-4e3a-aa24-312da9a468ab")] -------------------------------------------------------------------------------- /src/Webhooks.OrmLite/Webhooks.OrmLite.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard20;net472 5 | Library 6 | 7 | ServiceStack.Webhooks.OrmLite 8 | ServiceStack.Webhooks.OrmLite 9 | Debug;Release;ReleaseNoTestDeploy 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 5.9.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Webhooks.OrmLite/Webhooks.OrmLite.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.Relays.UnitTests/CacheClientEventSubscriptionCacheSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Moq; 4 | using NUnit.Framework; 5 | using ServiceStack.Caching; 6 | using ServiceStack.Webhooks.ServiceModel.Types; 7 | 8 | namespace ServiceStack.Webhooks.Relays.UnitTests 9 | { 10 | public class CacheClientEventSubscriptionCacheSpec 11 | { 12 | [TestFixture] 13 | public class GivenAContext 14 | { 15 | private CacheClientEventSubscriptionCache cache; 16 | private Mock cacheClient; 17 | private Mock subscriptionService; 18 | 19 | [SetUp] 20 | public void Initialize() 21 | { 22 | subscriptionService = new Mock(); 23 | cacheClient = new Mock(); 24 | cache = new CacheClientEventSubscriptionCache 25 | { 26 | SubscriptionService = subscriptionService.Object, 27 | CacheClient = cacheClient.Object 28 | }; 29 | } 30 | 31 | [Test, Category("Unit")] 32 | public void WhenGetAllWithNullEventName_ThenThrows() 33 | { 34 | Assert.Throws(() => cache.GetAll(null)); 35 | } 36 | 37 | [Test, Category("Unit")] 38 | public void WhenGetAllAndNothingInCache_ThenCachesSubscriptions() 39 | { 40 | var config = new SubscriptionRelayConfig(); 41 | var subscribers = new List 42 | { 43 | config 44 | }; 45 | cacheClient.Setup(cc => cc.Get(It.IsAny())) 46 | .Returns((CachedSubscription) null); 47 | subscriptionService.Setup(ss => ss.Search(It.IsAny())) 48 | .Returns(subscribers); 49 | 50 | var result = cache.GetAll("aneventname"); 51 | 52 | Assert.That(result.Count, Is.EqualTo(1)); 53 | Assert.That(result[0], Is.EqualTo(config)); 54 | 55 | cacheClient.Verify(cc => cc.Get(CacheClientEventSubscriptionCache.FormatCacheKey("aneventname"))); 56 | subscriptionService.Verify(ss => ss.Search("aneventname")); 57 | cacheClient.Verify(cc => cc.Set(CacheClientEventSubscriptionCache.FormatCacheKey("aneventname"), It.Is(cs => 58 | cs.Subscribers == subscribers), TimeSpan.FromSeconds(cache.ExpiryTimeSeconds))); 59 | } 60 | 61 | [Test, Category("Unit")] 62 | public void WhenGetAllAndCached_ThenReturnsSubscriptions() 63 | { 64 | var config = new SubscriptionRelayConfig(); 65 | var subscribers = new List 66 | { 67 | config 68 | }; 69 | cacheClient.Setup(cc => cc.Get(It.IsAny())) 70 | .Returns(new CachedSubscription 71 | { 72 | Subscribers = subscribers 73 | }); 74 | 75 | var result = cache.GetAll("aneventname"); 76 | 77 | Assert.That(result.Count, Is.EqualTo(1)); 78 | Assert.That(result[0], Is.EqualTo(config)); 79 | 80 | cacheClient.Verify(cc => cc.Get(CacheClientEventSubscriptionCache.FormatCacheKey("aneventname"))); 81 | subscriptionService.Verify(ss => ss.Search(It.IsAny()), Times.Never); 82 | cacheClient.Verify(cc => cc.Set(It.IsAny(), It.IsAny()), Times.Never); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays.UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Relays.UnitTests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("fb568aa6-8ce0-409c-8b4a-25c8ac6cd108")] -------------------------------------------------------------------------------- /src/Webhooks.Relays.UnitTests/Webhooks.Relays.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.Relays.UnitTests 9 | ServiceStack.Webhooks.Relays.UnitTests 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Webhooks.Relays.UnitTests/Webhooks.Relays.UnitTests.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.Relays/CacheClientEventSubscriptionCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using ServiceStack.Caching; 5 | using ServiceStack.Webhooks.ServiceModel.Types; 6 | 7 | namespace ServiceStack.Webhooks.Relays 8 | { 9 | /// 10 | /// This cache stores subscriptions (by eventname) for some TTL before fetching them again from 11 | /// . 12 | /// 13 | public class CacheClientEventSubscriptionCache : IEventSubscriptionCache 14 | { 15 | internal const string CachekeyPrefix = @"subscribers"; 16 | internal const string CachekeyFormat = CachekeyPrefix + @":{0}"; 17 | internal const int DefaultCacheExpirySeconds = 60; 18 | 19 | public CacheClientEventSubscriptionCache() 20 | { 21 | ExpiryTimeSeconds = DefaultCacheExpirySeconds; 22 | } 23 | 24 | public ICacheClient CacheClient { get; set; } 25 | 26 | public ISubscriptionService SubscriptionService { get; set; } 27 | 28 | public int ExpiryTimeSeconds { get; set; } 29 | 30 | public List GetAll(string eventName) 31 | { 32 | Guard.AgainstNullOrEmpty(() => eventName, eventName); 33 | 34 | var cached = CacheClient.Get(FormatCacheKey(eventName)); 35 | if (cached != null) 36 | { 37 | return cached.Subscribers; 38 | } 39 | 40 | var fetched = SubscriptionService.Search(eventName); 41 | 42 | if (fetched.Any()) 43 | { 44 | var expiry = TimeSpan.FromSeconds(ExpiryTimeSeconds); 45 | CacheClient.Set(FormatCacheKey(eventName), new CachedSubscription 46 | { 47 | Subscribers = fetched 48 | }, expiry); 49 | } 50 | 51 | return fetched; 52 | } 53 | 54 | public static string FormatCacheKey(string eventName) 55 | { 56 | return CachekeyFormat.Fmt(eventName); 57 | } 58 | } 59 | 60 | public class CachedSubscription 61 | { 62 | public List Subscribers { get; set; } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/Clients/EventServiceClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using ServiceStack.Logging; 4 | using ServiceStack.Text; 5 | using ServiceStack.Webhooks.Clients; 6 | using ServiceStack.Webhooks.Relays.Properties; 7 | using ServiceStack.Webhooks.Security; 8 | using ServiceStack.Webhooks.ServiceModel.Types; 9 | 10 | namespace ServiceStack.Webhooks.Relays.Clients 11 | { 12 | public class EventServiceClient : IEventServiceClient 13 | { 14 | private const int DefaultRetries = 3; 15 | private const int DefaultTimeout = 60; 16 | private readonly ILog logger = LogManager.GetLogger(typeof(EventServiceClient)); 17 | 18 | public EventServiceClient() 19 | { 20 | Retries = DefaultRetries; 21 | Timeout = TimeSpan.FromSeconds(DefaultTimeout); 22 | } 23 | 24 | public IEventServiceClientFactory ServiceClientFactory { get; set; } 25 | 26 | public TimeSpan? Timeout { get; set; } 27 | 28 | public int Retries { get; set; } 29 | 30 | public SubscriptionDeliveryResult Relay(SubscriptionRelayConfig subscription, WebhookEvent webhookEvent) 31 | { 32 | Guard.AgainstNull(() => subscription, subscription); 33 | Guard.AgainstNull(() => webhookEvent, webhookEvent); 34 | 35 | var serviceClient = CreateServiceClient(subscription, webhookEvent.EventName, Timeout); 36 | 37 | var attempts = 0; 38 | 39 | while (attempts <= Retries) 40 | { 41 | attempts++; 42 | 43 | try 44 | { 45 | var url = subscription.Config.Url; 46 | logger.InfoFormat("[ServiceStack.Webhooks.Relays.Clients.EventServiceClient] Notifying {0} of webhook event {1}", url, webhookEvent.ToJson()); 47 | using (var response = serviceClient.Post(url, webhookEvent.Data)) 48 | { 49 | return CreateDeliveryResult(subscription.SubscriptionId, webhookEvent.Id, response.StatusCode, response.StatusDescription); 50 | } 51 | } 52 | catch (WebServiceException ex) 53 | { 54 | if (HasNoMoreRetries(attempts) || ex.IsAny400()) 55 | { 56 | logger.Warn("[ServiceStack.Webhooks.Relays.Clients.EventServiceClient] " + Resources.EventServiceClient_FailedDelivery.Fmt(subscription.Config.Url, attempts), ex); 57 | return CreateDeliveryResult(subscription.SubscriptionId, webhookEvent.Id, (HttpStatusCode) ex.StatusCode, ex.StatusDescription); 58 | } 59 | } 60 | catch (Exception ex) 61 | { 62 | // Timeout (WebException) or other Exception 63 | if (HasNoMoreRetries(attempts)) 64 | { 65 | var message = "[ServiceStack.Webhooks.Relays.Clients.EventServiceClient] " + Resources.EventServiceClient_FailedDelivery.Fmt(subscription.Config.Url, attempts); 66 | logger.Warn(message, ex); 67 | return CreateDeliveryResult(subscription.SubscriptionId, webhookEvent.Id, HttpStatusCode.ServiceUnavailable, message); 68 | } 69 | } 70 | } 71 | 72 | return null; 73 | } 74 | 75 | private static SubscriptionDeliveryResult CreateDeliveryResult(string subscriptionId, string messageId, HttpStatusCode statusCode, string statusDescription) 76 | { 77 | return new SubscriptionDeliveryResult 78 | { 79 | Id = DataFormats.CreateEntityIdentifier(), 80 | AttemptedDateUtc = SystemTime.UtcNow.ToNearestMillisecond(), 81 | SubscriptionId = subscriptionId, 82 | StatusDescription = statusDescription, 83 | StatusCode = statusCode, 84 | EventId = messageId, 85 | }; 86 | } 87 | 88 | private bool HasNoMoreRetries(int attempts) 89 | { 90 | return attempts == Retries; 91 | } 92 | 93 | /// 94 | /// Creates an instance of a serviceclient and configures it to send an event notification. 95 | /// See: https://developer.github.com/webhooks/#payloads for specification 96 | /// 97 | private IServiceClient CreateServiceClient(SubscriptionRelayConfig relayConfig, string eventName, TimeSpan? timeout) 98 | { 99 | try 100 | { 101 | var client = ServiceClientFactory.Create(relayConfig.Config.Url); 102 | client.Timeout = timeout; 103 | client.RequestFilter = request => 104 | { 105 | request.ContentType = MimeTypes.Json; 106 | request.Headers.Remove(WebhookEventConstants.SecretSignatureHeaderName); 107 | request.Headers.Remove(WebhookEventConstants.RequestIdHeaderName); 108 | request.Headers.Remove(WebhookEventConstants.EventNameHeaderName); 109 | 110 | if (relayConfig.Config.ContentType.HasValue()) 111 | { 112 | request.ContentType = relayConfig.Config.ContentType; 113 | } 114 | request.Headers.Add(WebhookEventConstants.RequestIdHeaderName, CreateRequestIdentifier()); 115 | request.Headers.Add(WebhookEventConstants.EventNameHeaderName, eventName); 116 | }; 117 | client.OnSerializeRequest = (httpRequest, body, request) => 118 | { 119 | if (relayConfig.Config.Secret.HasValue()) 120 | { 121 | var hash = body.CreateHmacSignature(relayConfig.Config.Secret); 122 | httpRequest.Headers.Add(WebhookEventConstants.SecretSignatureHeaderName, hash); 123 | } 124 | }; 125 | return client; 126 | } 127 | catch (Exception ex) 128 | { 129 | logger.Error(@"[ServiceStack.Webhooks.Relays.Clients.EventServiceClient] Failed to connect to subscriber: {0}, the URL is not valid".Fmt(relayConfig.Config.Url), ex); 130 | return null; 131 | } 132 | } 133 | 134 | private static string CreateRequestIdentifier() 135 | { 136 | return Guid.NewGuid().ToString("N"); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/Clients/EventServiceClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack.Logging; 3 | 4 | namespace ServiceStack.Webhooks.Relays.Clients 5 | { 6 | public class EventServiceClientFactory : IEventServiceClientFactory 7 | { 8 | private readonly ILog logger = LogManager.GetLogger(typeof(EventServiceClientFactory)); 9 | 10 | public IServiceClient Create(string url) 11 | { 12 | try 13 | { 14 | return new ServiceClient(url); 15 | } 16 | catch (Exception ex) 17 | { 18 | logger.Error(@"[ServiceStack.Webhooks.Relays.Clients.EventServiceClientFactory] Failed to create a serviceclient to subscriber at {0}".Fmt(url), ex); 19 | return null; 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/Clients/IEventServiceClientFactory.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks.Relays.Clients 2 | { 3 | public interface IEventServiceClientFactory 4 | { 5 | IServiceClient Create(string url); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/Clients/IServiceClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace ServiceStack.Webhooks.Relays.Clients 5 | { 6 | public interface IServiceClient 7 | { 8 | /// 9 | /// Gets or sets the timeout allowed for returning a response 10 | /// 11 | TimeSpan? Timeout { get; set; } 12 | 13 | /// 14 | /// Gets or sets the filter to run when a request is made 15 | /// 16 | Action RequestFilter { get; set; } 17 | 18 | /// 19 | /// Gets or sets the filter to run when the request is about to be serialized 20 | /// 21 | Action OnSerializeRequest { get; set; } 22 | 23 | /// 24 | /// Gets or sets an action to perform when authentication is required by the request 25 | /// 26 | Action OnAuthenticationRequired { get; set; } 27 | 28 | /// 29 | /// Gets or sets the cookies to use in requests 30 | /// 31 | CookieContainer CookieContainer { get; set; } 32 | 33 | /// 34 | /// Gets or sets the bearer token to use in requests 35 | /// 36 | string BearerToken { get; set; } 37 | 38 | /// 39 | /// Posts the specified data to the specified URL 40 | /// 41 | HttpWebResponse Post(string url, TRequest data); 42 | 43 | /// 44 | /// Returns the response for a GET request 45 | /// 46 | TResponse Get(IReturn request); 47 | 48 | /// 49 | /// Puts the specified data to the specified URL 50 | /// 51 | TResponse Put(IReturn request); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/Clients/ServiceClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using ServiceStack.Text; 4 | 5 | namespace ServiceStack.Webhooks.Relays.Clients 6 | { 7 | internal class ServiceClient : JsonServiceClient, IServiceClient 8 | { 9 | public ServiceClient(string url) : base(url) 10 | { 11 | } 12 | 13 | public HttpWebResponse Post(string url, TRequest request) 14 | { 15 | return Post(url, request); 16 | } 17 | 18 | public Action OnSerializeRequest { get; set; } 19 | 20 | protected override WebRequest SendRequest(string httpMethod, string requestUri, object request) 21 | { 22 | if (OnSerializeRequest == null) 23 | { 24 | return base.SendRequest(httpMethod, requestUri, request); 25 | } 26 | 27 | return PrepareWebRequest(httpMethod, requestUri, request, client => 28 | { 29 | using (var tempStream = MemoryStreamFactory.GetStream()) 30 | using (var requestStream = PclExport.Instance.GetRequestStream(client)) 31 | { 32 | SerializeRequestToStream(request, tempStream, true); 33 | var bytes = tempStream.ToArray(); 34 | 35 | OnSerializeRequest(client, bytes, request); 36 | 37 | requestStream.Write(bytes, 0, bytes.Length); 38 | } 39 | }); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/ISubscriptionService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceStack.Webhooks.ServiceModel.Types; 3 | 4 | namespace ServiceStack.Webhooks.Relays 5 | { 6 | public interface ISubscriptionService 7 | { 8 | List Search(string eventName); 9 | 10 | void UpdateResults(List results); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Webhooks.Relays/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Relays")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: Guid("1e47701b-1779-4eee-8b32-8112c283d6f7")] -------------------------------------------------------------------------------- /src/Webhooks.Relays/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ServiceStack.Webhooks.Relays.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ServiceStack.Webhooks.Relays.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Failed to notify subscriber at {0} (after {1} attempts). 65 | /// 66 | public static string EventServiceClient_FailedDelivery { 67 | get { 68 | return ResourceManager.GetString("EventServiceClient_FailedDelivery", resourceCulture); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Webhooks.Relays/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Failed to notify subscriber at {0} (after {1} attempts) 122 | 123 | -------------------------------------------------------------------------------- /src/Webhooks.Relays/Webhooks.Relays.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard20;net472 5 | Library 6 | 7 | ServiceStack.Webhooks.Relays 8 | ServiceStack.Webhooks.Relays 9 | Debug;Release;ReleaseNoTestDeploy 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 5.9.0 21 | 22 | 23 | 24 | 25 | True 26 | True 27 | Resources.resx 28 | 29 | 30 | 31 | 32 | PublicResXFileCodeGenerator 33 | Resources.Designer.cs 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Webhooks.Relays/Webhooks.Relays.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.Subscribers.UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Subscribers.UnitTests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("35e2ffec-7789-4e1a-8306-5243e2a15cb2")] -------------------------------------------------------------------------------- /src/Webhooks.Subscribers.UnitTests/Webhooks.Subscribers.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.Subscribers.UnitTests 9 | ServiceStack.Webhooks.Subscribers.UnitTests 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Webhooks.Subscribers.UnitTests/Webhooks.Subscribers.UnitTests.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.Subscribers/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.Subscribers")] 5 | [assembly: AssemblyDescription("Add Webhooks to your ServiceStack services")] 6 | [assembly: Guid("670bbdd7-0cd5-4049-ac74-83e97c20df38")] -------------------------------------------------------------------------------- /src/Webhooks.Subscribers/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ServiceStack.Webhooks.Subscribers.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ServiceStack.Webhooks.Subscribers.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The secret has not been correctly configured. 65 | /// 66 | internal static string HmacAuthProvider_IncorrectlyConfigured { 67 | get { 68 | return ResourceManager.GetString("HmacAuthProvider_IncorrectlyConfigured", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to The connection must be secured with HTTPS. 74 | /// 75 | internal static string HmacAuthProvider_NotHttps { 76 | get { 77 | return ResourceManager.GetString("HmacAuthProvider_NotHttps", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Webhooks.Subscribers/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The secret has not been correctly configured 122 | 123 | 124 | The connection must be secured with HTTPS 125 | 126 | -------------------------------------------------------------------------------- /src/Webhooks.Subscribers/Security/HmacAuthProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using ServiceStack.Auth; 4 | using ServiceStack.Configuration; 5 | using ServiceStack.Text; 6 | using ServiceStack.Web; 7 | using ServiceStack.Webhooks.Security; 8 | using ServiceStack.Webhooks.Subscribers.Properties; 9 | 10 | namespace ServiceStack.Webhooks.Subscribers.Security 11 | { 12 | public class HmacAuthProvider : AuthProvider, IAuthWithRequest 13 | { 14 | public const string Name = "hmac"; 15 | public const string Realm = "/auth/hmac"; 16 | public const string SecretSettingsName = "hmac.Secret"; 17 | 18 | public HmacAuthProvider() 19 | : base(null, Realm, Name) 20 | { 21 | RequireSecureConnection = true; 22 | } 23 | 24 | public HmacAuthProvider(IAppSettings settings) 25 | : this() 26 | { 27 | Guard.AgainstNull(() => settings, settings); 28 | 29 | Secret = settings.GetString(SecretSettingsName); 30 | 31 | RequireSecureConnection = true; 32 | } 33 | 34 | public string Secret { get; set; } 35 | 36 | public Func OnGetSecret { get; set; } 37 | 38 | public bool RequireSecureConnection { get; set; } 39 | 40 | public void PreAuthenticate(IRequest req, IResponse res) 41 | { 42 | var signature = req.Headers[WebhookEventConstants.SecretSignatureHeaderName]; 43 | if (signature.HasValue()) 44 | { 45 | if (RequireSecureConnection && !req.IsSecureConnection) 46 | { 47 | throw HttpError.Forbidden(Resources.HmacAuthProvider_NotHttps); 48 | } 49 | 50 | var eventName = req.Headers[WebhookEventConstants.EventNameHeaderName]; 51 | if (OnGetSecret != null) 52 | { 53 | Secret = OnGetSecret(req, eventName); 54 | } 55 | if (!Secret.HasValue()) 56 | { 57 | throw HttpError.Unauthorized(Resources.HmacAuthProvider_IncorrectlyConfigured); 58 | } 59 | 60 | var isValidSecret = req.VerifySignature(signature, Secret); 61 | if (!isValidSecret) 62 | { 63 | throw new HttpError(HttpStatusCode.Unauthorized); 64 | } 65 | 66 | var requestId = req.Headers[WebhookEventConstants.RequestIdHeaderName]; 67 | var userId = requestId.HasValue() ? requestId : Guid.NewGuid().ToString("N"); 68 | var username = req.GetUrlHostName(); 69 | 70 | var sessionId = SessionExtensions.CreateRandomSessionId(); 71 | var session = SessionFeature.CreateNewSession(req, sessionId); 72 | session.UserAuthId = userId; 73 | session.UserAuthName = username; 74 | session.UserName = username; 75 | session.IsAuthenticated = true; 76 | session.CreatedAt = SystemTime.UtcNow; 77 | 78 | HostContext.AppHost.OnSessionFilter(req, session, sessionId); 79 | 80 | req.Items[Keywords.Session] = session; 81 | } 82 | } 83 | 84 | public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null) 85 | { 86 | return session != null 87 | && session.IsAuthenticated 88 | && !session.UserAuthId.IsNullOrEmpty(); 89 | } 90 | 91 | public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request) 92 | { 93 | throw new NotImplementedException(@"HmacAuthProvider.Authenticate should never be called."); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/Webhooks.Subscribers/Webhooks.Subscribers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard20;net472 5 | Library 6 | 7 | ServiceStack.Webhooks.Subscribers 8 | ServiceStack.Webhooks.Subscribers 9 | Debug;Release;ReleaseNoTestDeploy 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 5.9.0 21 | 22 | 23 | 24 | 25 | True 26 | True 27 | Resources.resx 28 | 29 | 30 | 31 | 32 | ResXFileCodeGenerator 33 | Resources.Designer.cs 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Webhooks.Subscribers/Webhooks.Subscribers.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/AppHostEventSinkSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Moq; 4 | using NUnit.Framework; 5 | using ServiceStack.Webhooks.Clients; 6 | using ServiceStack.Webhooks.Relays; 7 | using ServiceStack.Webhooks.ServiceModel.Types; 8 | 9 | namespace ServiceStack.Webhooks.UnitTests 10 | { 11 | public class AppHostEventSinkSpec 12 | { 13 | [TestFixture] 14 | public class GivenACacheAndServiceClient 15 | { 16 | private Mock serviceClient; 17 | private AppHostEventSink sink; 18 | private Mock subscriptionCache; 19 | private Mock subscriptionService; 20 | 21 | [SetUp] 22 | public void Initialize() 23 | { 24 | serviceClient = new Mock(); 25 | subscriptionCache = new Mock(); 26 | subscriptionService = new Mock(); 27 | sink = new AppHostEventSink 28 | { 29 | ServiceClient = serviceClient.Object, 30 | SubscriptionCache = subscriptionCache.Object, 31 | SubscriptionService = subscriptionService.Object 32 | }; 33 | } 34 | 35 | [Test, Category("Unit")] 36 | public void WhenWriteWithNullEvent_ThenThrows() 37 | { 38 | Assert.That(() => sink.Write(null), Throws.ArgumentNullException); 39 | } 40 | 41 | [Test, Category("Unit")] 42 | public void WhenWriteWithNoSubscriptions_ThenIgnoresEvent() 43 | { 44 | subscriptionCache.Setup(sc => sc.GetAll(It.IsAny())) 45 | .Returns(new List()); 46 | 47 | sink.Write(new WebhookEvent 48 | { 49 | EventName = "aneventname" 50 | }); 51 | 52 | subscriptionCache.Verify(sc => sc.GetAll("aneventname")); 53 | serviceClient.Verify(sc => sc.Relay(It.IsAny(), It.IsAny()), Times.Never); 54 | } 55 | 56 | [Test, Category("Unit")] 57 | public void WhenWrite_ThenPostsEventToSubscribers() 58 | { 59 | var config = new SubscriptionRelayConfig(); 60 | subscriptionCache.Setup(sc => sc.GetAll(It.IsAny())) 61 | .Returns(new List 62 | { 63 | config 64 | }); 65 | var whe = new WebhookEvent 66 | { 67 | EventName = "aneventname" 68 | }; 69 | sink.Write(whe); 70 | 71 | subscriptionCache.Verify(sc => sc.GetAll("aneventname")); 72 | serviceClient.VerifySet(sc => sc.Retries = AppHostEventSink.DefaultServiceClientRetries); 73 | serviceClient.VerifySet(sc => sc.Timeout = TimeSpan.FromSeconds(AppHostEventSink.DefaultServiceClientTimeoutSeconds)); 74 | serviceClient.Verify(sc => sc.Relay(config, whe)); 75 | } 76 | 77 | [Test, Category("Unit")] 78 | public void WhenWrite_ThenPostsEventToSubscribersAndUpdatesResults() 79 | { 80 | var config = new SubscriptionRelayConfig(); 81 | subscriptionCache.Setup(sc => sc.GetAll(It.IsAny())) 82 | .Returns(new List 83 | { 84 | config 85 | }); 86 | var result = new SubscriptionDeliveryResult(); 87 | var whe = new WebhookEvent 88 | { 89 | EventName = "aneventname" 90 | }; 91 | serviceClient.Setup(sc => sc.Relay(config, whe)) 92 | .Returns(result); 93 | 94 | sink.Write(whe); 95 | 96 | subscriptionService.Verify(ss => ss.UpdateResults(It.Is>(results => 97 | results.Count == 1 98 | && results[0] == result))); 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks.UnitTests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: Guid("d1bedb76-4bd5-4032-baa2-bc99c21ac031")] -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/CreateSubscriptionValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Moq; 3 | using NUnit.Framework; 4 | using ServiceStack.FluentValidation; 5 | using ServiceStack.FluentValidation.Results; 6 | using ServiceStack.Webhooks.Properties; 7 | using ServiceStack.Webhooks.ServiceModel; 8 | using ServiceStack.Webhooks.ServiceModel.Types; 9 | 10 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 11 | { 12 | public class CreateSubscriptionValidatorSpec 13 | { 14 | [TestFixture] 15 | public class GivenADto 16 | { 17 | private CreateSubscription dto; 18 | private Mock eventsValidator; 19 | private Mock subscriptionConfigValidator; 20 | private CreateSubscriptionValidator validator; 21 | 22 | [SetUp] 23 | public void Initialize() 24 | { 25 | dto = new CreateSubscription 26 | { 27 | Name = "aname", 28 | Events = new List(), 29 | Config = new SubscriptionConfig() 30 | }; 31 | subscriptionConfigValidator = new Mock(); 32 | subscriptionConfigValidator.Setup(val => val.Validate(It.IsAny())) 33 | .Returns(new ValidationResult()); 34 | eventsValidator = new Mock(); 35 | eventsValidator.Setup(val => val.Validate(It.IsAny())) 36 | .Returns(new ValidationResult()); 37 | validator = new CreateSubscriptionValidator(eventsValidator.Object, subscriptionConfigValidator.Object); 38 | } 39 | 40 | [Test, Category("Unit")] 41 | public void WhenAllPropertiesValid_ThenSucceeds() 42 | { 43 | validator.ValidateAndThrow(dto); 44 | } 45 | 46 | [Test, Category("Unit")] 47 | public void WhenNameIsNull_ThenThrows() 48 | { 49 | dto.Name = null; 50 | 51 | validator.Validate(dto); 52 | 53 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.CreateSubscriptionValidator_InvalidName)); 54 | } 55 | 56 | [Test, Category("Unit")] 57 | public void WhenNameIsInvalid_ThenThrows() 58 | { 59 | dto.Name = "^"; 60 | 61 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.CreateSubscriptionValidator_InvalidName)); 62 | } 63 | 64 | [Test, Category("Unit")] 65 | public void WhenEventsIsNull_ThenThrows() 66 | { 67 | dto.Events = null; 68 | 69 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.CreateSubscriptionValidator_InvalidEvents)); 70 | } 71 | 72 | [Test, Category("Unit")] 73 | public void WhenConfigIsNull_ThenThrows() 74 | { 75 | dto.Config = null; 76 | 77 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.CreateSubscriptionValidator_InvalidConfig)); 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/DeleteSubscriptionValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.FluentValidation; 3 | using ServiceStack.Webhooks.Properties; 4 | using ServiceStack.Webhooks.ServiceModel; 5 | 6 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 7 | { 8 | public class DeleteSubscriptionValidatorSpec 9 | { 10 | [TestFixture] 11 | public class GivenADto 12 | { 13 | private DeleteSubscription dto; 14 | private DeleteSubscriptionValidator validator; 15 | 16 | [SetUp] 17 | public void Initialize() 18 | { 19 | dto = new DeleteSubscription 20 | { 21 | Id = DataFormats.CreateEntityIdentifier() 22 | }; 23 | validator = new DeleteSubscriptionValidator(); 24 | } 25 | 26 | [Test, Category("Unit")] 27 | public void WhenAllPropertiesValid_ThenSucceeds() 28 | { 29 | validator.ValidateAndThrow(dto); 30 | } 31 | 32 | [Test, Category("Unit")] 33 | public void WhenIdIsNull_ThenThrows() 34 | { 35 | dto.Id = null; 36 | 37 | validator.Validate(dto); 38 | 39 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.DeleteSubscriptionValidator_InvalidId)); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/GetSubscriptionValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.FluentValidation; 3 | using ServiceStack.Webhooks.Properties; 4 | using ServiceStack.Webhooks.ServiceModel; 5 | 6 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 7 | { 8 | public class GetSubscriptionValidatorSpec 9 | { 10 | [TestFixture] 11 | public class GivenADto 12 | { 13 | private GetSubscription dto; 14 | private GetSubscriptionValidator validator; 15 | 16 | [SetUp] 17 | public void Initialize() 18 | { 19 | dto = new GetSubscription 20 | { 21 | Id = DataFormats.CreateEntityIdentifier() 22 | }; 23 | validator = new GetSubscriptionValidator(); 24 | } 25 | 26 | [Test, Category("Unit")] 27 | public void WhenAllPropertiesValid_ThenSucceeds() 28 | { 29 | validator.ValidateAndThrow(dto); 30 | } 31 | 32 | [Test, Category("Unit")] 33 | public void WhenIdIsNull_ThenThrows() 34 | { 35 | dto.Id = null; 36 | 37 | validator.Validate(dto); 38 | 39 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.GetSubscriptionValidator_InvalidId)); 40 | } 41 | 42 | [Test, Category("Unit")] 43 | public void WhenIdIsInvalid_ThenThrows() 44 | { 45 | dto.Id = "anid"; 46 | 47 | validator.Validate(dto); 48 | 49 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.GetSubscriptionValidator_InvalidId)); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/ListSubscriptionsValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.FluentValidation; 3 | using ServiceStack.Webhooks.ServiceModel; 4 | 5 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 6 | { 7 | public class ListSubscriptionsValidatorSpec 8 | { 9 | [TestFixture] 10 | public class GivenADto 11 | { 12 | private ListSubscriptions dto; 13 | private ListSubscriptionsValidator validator; 14 | 15 | [SetUp] 16 | public void Initialize() 17 | { 18 | dto = new ListSubscriptions(); 19 | validator = new ListSubscriptionsValidator(); 20 | } 21 | 22 | [Test, Category("Unit")] 23 | public void WhenAllPropertiesValid_ThenSucceeds() 24 | { 25 | validator.ValidateAndThrow(dto); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/SearchSubscriptionsValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.FluentValidation; 3 | using ServiceStack.Webhooks.Properties; 4 | using ServiceStack.Webhooks.ServiceModel; 5 | 6 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 7 | { 8 | public class SearchSubscriptionsValidatorSpec 9 | { 10 | [TestFixture] 11 | public class GivenADto 12 | { 13 | private SearchSubscriptions dto; 14 | private SearchSubscriptionsValidator validator; 15 | 16 | [SetUp] 17 | public void Initialize() 18 | { 19 | dto = new SearchSubscriptions 20 | { 21 | EventName = "aneventname" 22 | }; 23 | validator = new SearchSubscriptionsValidator(); 24 | } 25 | 26 | [Test, Category("Unit")] 27 | public void WhenAllPropertiesValid_ThenSucceeds() 28 | { 29 | validator.ValidateAndThrow(dto); 30 | } 31 | 32 | [Test, Category("Unit")] 33 | public void WhenEventNameIsNull_ThenThrows() 34 | { 35 | dto.EventName = null; 36 | 37 | validator.Validate(dto); 38 | 39 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SearchSubscriptionsValidator_InvalidEventName)); 40 | } 41 | 42 | [Test, Category("Unit")] 43 | public void WhenEventNameIsInvalid_ThenThrows() 44 | { 45 | dto.EventName = "^"; 46 | 47 | validator.Validate(dto); 48 | 49 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SearchSubscriptionsValidator_InvalidEventName)); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/SubscriptionConfigValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using NUnit.Framework; 4 | using ServiceStack.FluentValidation; 5 | using ServiceStack.Webhooks.Properties; 6 | using ServiceStack.Webhooks.ServiceModel; 7 | using ServiceStack.Webhooks.ServiceModel.Types; 8 | 9 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 10 | { 11 | public class SubscriptionConfigValidatorSpec 12 | { 13 | [TestFixture] 14 | public class GivenADto 15 | { 16 | private SubscriptionConfig dto; 17 | private SubscriptionConfigValidator validator; 18 | 19 | [SetUp] 20 | public void Initialize() 21 | { 22 | dto = new SubscriptionConfig 23 | { 24 | Url = "http://localhost", 25 | ContentType = MimeTypes.Json, 26 | Secret = Convert.ToBase64String(Encoding.Default.GetBytes("asecret")) 27 | }; 28 | validator = new SubscriptionConfigValidator(); 29 | } 30 | 31 | [Test, Category("Unit")] 32 | public void WhenAllPropertiesValid_ThenSucceeds() 33 | { 34 | validator.ValidateAndThrow(dto); 35 | } 36 | 37 | [Test, Category("Unit")] 38 | public void WhenUrlIsNull_ThenThrows() 39 | { 40 | dto.Url = null; 41 | 42 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_InvalidUrl)); 43 | } 44 | 45 | [Test, Category("Unit")] 46 | public void WhenUrlIsNotAUrl_ThenThrows() 47 | { 48 | dto.Url = "aurl"; 49 | 50 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_InvalidUrl)); 51 | } 52 | 53 | [Test, Category("Unit")] 54 | public void WhenContentTypeIsNull_ThenSucceeds() 55 | { 56 | dto.ContentType = null; 57 | 58 | validator.ValidateAndThrow(dto); 59 | } 60 | 61 | [Test, Category("Unit")] 62 | public void WhenContentTypeIsJson_ThenSucceeds() 63 | { 64 | dto.ContentType = MimeTypes.Json; 65 | 66 | validator.Validate(dto); 67 | 68 | validator.ValidateAndThrow(dto); 69 | } 70 | 71 | [Test, Category("Unit")] 72 | public void WhenContentTypeIsNotJson_ThenThrows() 73 | { 74 | dto.ContentType = "notjson"; 75 | 76 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_UnsupportedContentType)); 77 | } 78 | 79 | [Test, Category("Unit")] 80 | public void WhenSecretIsNull_ThenSucceeds() 81 | { 82 | dto.Secret = null; 83 | 84 | validator.ValidateAndThrow(dto); 85 | } 86 | 87 | [Test, Category("Unit")] 88 | public void WhenSecretIsBase64_ThenSucceeds() 89 | { 90 | dto.Secret = new string('A', 1000); 91 | 92 | validator.ValidateAndThrow(dto); 93 | } 94 | 95 | [Test, Category("Unit")] 96 | public void WhenSecretIsInvalid_ThenThrows() 97 | { 98 | dto.Secret = new string('A', 1000 + 1); 99 | 100 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_InvalidSecret)); 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/SubscriptionDeliveryResultValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using ServiceStack.FluentValidation; 3 | using ServiceStack.Webhooks.Properties; 4 | using ServiceStack.Webhooks.ServiceModel; 5 | using ServiceStack.Webhooks.ServiceModel.Types; 6 | 7 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 8 | { 9 | public class SubscriptionDeliveryResultValidatorSpec 10 | { 11 | [TestFixture] 12 | public class GivenADto 13 | { 14 | private SubscriptionDeliveryResult dto; 15 | private SubscriptionDeliveryResultValidator validator; 16 | 17 | [SetUp] 18 | public void Initialize() 19 | { 20 | dto = new SubscriptionDeliveryResult 21 | { 22 | Id = DataFormats.CreateEntityIdentifier(), 23 | SubscriptionId = DataFormats.CreateEntityIdentifier() 24 | }; 25 | validator = new SubscriptionDeliveryResultValidator(); 26 | } 27 | 28 | [Test, Category("Unit")] 29 | public void WhenAllPropertiesValid_ThenSucceeds() 30 | { 31 | validator.ValidateAndThrow(dto); 32 | } 33 | 34 | [Test, Category("Unit")] 35 | public void WhenIdIsNull_ThenThrows() 36 | { 37 | dto.Id = null; 38 | 39 | validator.Validate(dto); 40 | 41 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionDeliveryResultValidator_InvalidId)); 42 | } 43 | 44 | [Test, Category("Unit")] 45 | public void WhenIdIsInvalid_ThenThrows() 46 | { 47 | dto.Id = "anid"; 48 | 49 | validator.Validate(dto); 50 | 51 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionDeliveryResultValidator_InvalidId)); 52 | } 53 | 54 | [Test, Category("Unit")] 55 | public void WhenSubscriptionIdIsNull_ThenThrows() 56 | { 57 | dto.SubscriptionId = null; 58 | 59 | validator.Validate(dto); 60 | 61 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionDeliveryResultValidator_InvalidSubscriptionId)); 62 | } 63 | 64 | [Test, Category("Unit")] 65 | public void WhenSubscriptionIdIsInvalid_ThenThrows() 66 | { 67 | dto.SubscriptionId = "anid"; 68 | 69 | validator.Validate(dto); 70 | 71 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionDeliveryResultValidator_InvalidSubscriptionId)); 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/SubscriptionEventsValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NUnit.Framework; 3 | using ServiceStack.FluentValidation; 4 | using ServiceStack.Webhooks.Properties; 5 | using ServiceStack.Webhooks.ServiceModel; 6 | 7 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 8 | { 9 | public class SubscriptionEventsValidatorSpec 10 | { 11 | [TestFixture] 12 | public class GivenADto 13 | { 14 | private List dto; 15 | private SubscriptionEventsValidator validator; 16 | 17 | [SetUp] 18 | public void Initialize() 19 | { 20 | dto = new List {"anevent"}; 21 | validator = new SubscriptionEventsValidator(); 22 | } 23 | 24 | [Test, Category("Unit")] 25 | public void WhenAllPropertiesValid_ThenSucceeds() 26 | { 27 | validator.ValidateAndThrow(dto); 28 | } 29 | 30 | [Test, Category("Unit")] 31 | public void WhenNoEvents_ThenThrows() 32 | { 33 | dto = new List(); 34 | 35 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionEventsValidator_NoName)); 36 | } 37 | 38 | [Test, Category("Unit")] 39 | public void WhenAnEventNameIsEmpty_ThenThrows() 40 | { 41 | dto = new List {""}; 42 | 43 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionEventsValidator_EmptyName)); 44 | } 45 | 46 | [Test, Category("Unit")] 47 | public void WhenAnEventNameIsInvalid_ThenThrows() 48 | { 49 | dto = new List {"^"}; 50 | 51 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionEventsValidator_InvalidName)); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/UpdateSubscriptionHistoryValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using NUnit.Framework; 3 | using ServiceStack.FluentValidation; 4 | using ServiceStack.Webhooks.ServiceModel; 5 | 6 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 7 | { 8 | public class UpdateSubscriptionHistoryValidatorSpec 9 | { 10 | [TestFixture] 11 | public class GivenADto 12 | { 13 | private Mock deliveryResultValidator; 14 | private UpdateSubscriptionHistory dto; 15 | private UpdateSubscriptionHistoryValidator validator; 16 | 17 | [SetUp] 18 | public void Initialize() 19 | { 20 | dto = new UpdateSubscriptionHistory(); 21 | deliveryResultValidator = new Mock(); 22 | validator = new UpdateSubscriptionHistoryValidator(deliveryResultValidator.Object); 23 | } 24 | 25 | [Test, Category("Unit")] 26 | public void WhenAllPropertiesValid_ThenSucceeds() 27 | { 28 | validator.ValidateAndThrow(dto); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/ServiceModel/UpdateSubscriptionValidatorSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using NUnit.Framework; 4 | using ServiceStack.FluentValidation; 5 | using ServiceStack.Webhooks.Properties; 6 | using ServiceStack.Webhooks.ServiceModel; 7 | 8 | namespace ServiceStack.Webhooks.UnitTests.ServiceModel 9 | { 10 | public class UpdateSubscriptionValidatorSpec 11 | { 12 | [TestFixture] 13 | public class GivenADto 14 | { 15 | private UpdateSubscription dto; 16 | private UpdateSubscriptionValidator validator; 17 | 18 | [SetUp] 19 | public void Initialize() 20 | { 21 | dto = new UpdateSubscription 22 | { 23 | Id = DataFormats.CreateEntityIdentifier(), 24 | Url = "http://localhost", 25 | ContentType = MimeTypes.Json, 26 | Secret = Convert.ToBase64String(Encoding.Default.GetBytes("asecret")) 27 | }; 28 | validator = new UpdateSubscriptionValidator(); 29 | } 30 | 31 | [Test, Category("Unit")] 32 | public void WhenAllPropertiesValid_ThenSucceeds() 33 | { 34 | validator.ValidateAndThrow(dto); 35 | } 36 | 37 | [Test, Category("Unit")] 38 | public void WhenIdIsNull_ThenThrows() 39 | { 40 | dto.Id = null; 41 | 42 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.UpdateSubscriptionValidator_InvalidId)); 43 | } 44 | 45 | [Test, Category("Unit")] 46 | public void WhenUrlIsNull_ThenSucceeds() 47 | { 48 | dto.Url = null; 49 | 50 | validator.ValidateAndThrow(dto); 51 | } 52 | 53 | [Test, Category("Unit")] 54 | public void WhenUrlIsNotAUrl_ThenThrows() 55 | { 56 | dto.Url = "aurl"; 57 | 58 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_InvalidUrl)); 59 | } 60 | 61 | [Test, Category("Unit")] 62 | public void WhenContentTypeIsNull_ThenSucceeds() 63 | { 64 | dto.ContentType = null; 65 | 66 | validator.ValidateAndThrow(dto); 67 | } 68 | 69 | [Test, Category("Unit")] 70 | public void WhenContentTypeIsJson_ThenSucceeds() 71 | { 72 | dto.ContentType = MimeTypes.Json; 73 | 74 | validator.Validate(dto); 75 | 76 | validator.ValidateAndThrow(dto); 77 | } 78 | 79 | [Test, Category("Unit")] 80 | public void WhenContentTypeIsNotJson_ThenThrows() 81 | { 82 | dto.ContentType = "notjson"; 83 | 84 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_UnsupportedContentType)); 85 | } 86 | 87 | [Test, Category("Unit")] 88 | public void WhenSecretIsNull_ThenSucceeds() 89 | { 90 | dto.Secret = null; 91 | 92 | validator.ValidateAndThrow(dto); 93 | } 94 | 95 | [Test, Category("Unit")] 96 | public void WhenSecretIsBase64_ThenSucceeds() 97 | { 98 | dto.Secret = new string('A', 1000); 99 | 100 | validator.ValidateAndThrow(dto); 101 | } 102 | 103 | [Test, Category("Unit")] 104 | public void WhenSecretIsInvalid_ThenThrows() 105 | { 106 | dto.Secret = new string('A', 1000 + 1); 107 | 108 | Assert.That(() => validator.ValidateAndThrow(dto), Throws.TypeOf().With.Message.Contain(Resources.SubscriptionConfigValidator_InvalidSecret)); 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/Webhooks.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | net472 6 | Library 7 | 8 | ServiceStack.Webhooks.UnitTests 9 | ServiceStack.Webhooks.UnitTests 10 | Debug;Release;ReleaseNoTestDeploy 11 | false 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/Webhooks.UnitTests.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks.UnitTests/WebhooksClientSpec.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Moq; 3 | using NUnit.Framework; 4 | 5 | namespace ServiceStack.Webhooks.UnitTests 6 | { 7 | public class WebhooksClientSpec 8 | { 9 | [TestFixture] 10 | public class GivenAContext 11 | { 12 | private Mock eventSink; 13 | private WebhooksClient webhooks; 14 | 15 | [SetUp] 16 | public void Initialize() 17 | { 18 | eventSink = new Mock(); 19 | webhooks = new WebhooksClient 20 | { 21 | EventSink = eventSink.Object 22 | }; 23 | } 24 | 25 | [Test, Category("Unit")] 26 | public void WhenPublishWithNullEventName_ThenThrows() 27 | { 28 | Assert.Throws(() => webhooks.Publish(null, new TestPoco())); 29 | } 30 | 31 | [Test, Category("Unit")] 32 | public void WhenPublish_ThenSinksEvent() 33 | { 34 | var poco = new TestPoco(); 35 | webhooks.Publish("aneventname", poco); 36 | 37 | eventSink.Verify(es => es.Write(It.Is(we => 38 | we.EventName == "aneventname" 39 | && we.Data == poco))); 40 | } 41 | 42 | [Test, Category("Unit")] 43 | public void WhenPublishAndPublishEventFilter_ThenAugmentsPublishedEvent() 44 | { 45 | var poco = new TestPoco(); 46 | var poco2 = new TestPoco(); 47 | webhooks.PublishFilter = @event => 48 | { 49 | @event.Id = "anewid"; 50 | @event.Data = poco2; 51 | }; 52 | webhooks.Publish("aneventname", poco); 53 | 54 | eventSink.Verify(es => es.Write(It.Is(we => 55 | we.Id == "anewid" 56 | && we.EventName == "aneventname" 57 | && we.Data == poco2))); 58 | } 59 | } 60 | } 61 | 62 | public class TestPoco 63 | { 64 | } 65 | } -------------------------------------------------------------------------------- /src/Webhooks/AppHostEventSink.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using ServiceStack.Webhooks.Clients; 5 | using ServiceStack.Webhooks.Relays; 6 | using ServiceStack.Webhooks.ServiceModel.Types; 7 | 8 | namespace ServiceStack.Webhooks 9 | { 10 | public class AppHostEventSink : IEventSink 11 | { 12 | public const int DefaultServiceClientRetries = 3; 13 | public const int DefaultServiceClientTimeoutSeconds = 60; 14 | 15 | public AppHostEventSink() 16 | { 17 | Retries = DefaultServiceClientRetries; 18 | TimeoutSecs = DefaultServiceClientTimeoutSeconds; 19 | } 20 | 21 | public IEventSubscriptionCache SubscriptionCache { get; set; } 22 | 23 | public IEventServiceClient ServiceClient { get; set; } 24 | 25 | public ISubscriptionService SubscriptionService { get; set; } 26 | 27 | public int Retries { get; set; } 28 | 29 | public int TimeoutSecs { get; set; } 30 | 31 | public void Write(WebhookEvent webhookEvent) 32 | { 33 | Guard.AgainstNull(() => webhookEvent, webhookEvent); 34 | 35 | var subscriptions = SubscriptionCache.GetAll(webhookEvent.EventName); 36 | var results = new List(); 37 | subscriptions.ForEach(sub => 38 | { 39 | var result = NotifySubscription(sub, webhookEvent); 40 | if (result != null) 41 | { 42 | results.Add(result); 43 | } 44 | }); 45 | 46 | if (results.Any()) 47 | { 48 | SubscriptionService.UpdateResults(results); 49 | } 50 | } 51 | 52 | private SubscriptionDeliveryResult NotifySubscription(SubscriptionRelayConfig subscription, WebhookEvent webhookEvent) 53 | { 54 | ServiceClient.Retries = Retries; 55 | ServiceClient.Timeout = TimeSpan.FromSeconds(TimeoutSecs); 56 | return ServiceClient.Relay(subscription, webhookEvent); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Webhooks/AsyncHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ServiceStack.Webhooks 6 | { 7 | /// 8 | /// Credit to: https://cpratt.co/async-tips-tricks/ 9 | /// 10 | public static class AsyncHelper 11 | { 12 | public static void RunSync(Func func) 13 | { 14 | var taskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); 15 | taskFactory 16 | .StartNew(func) 17 | .GetAwaiter() 18 | .GetResult(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Webhooks/IWebhooks.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceStack.Webhooks 2 | { 3 | /// 4 | /// Defines the publisher of webhook events 5 | /// 6 | public interface IWebhooks 7 | { 8 | /// 9 | /// Publishes an event to all webhook subscribers 10 | /// 11 | void Publish(string eventName, TDto data) where TDto : class, new(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Webhooks/MemorySubscriptionStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using ServiceStack.Webhooks.ServiceModel.Types; 6 | 7 | namespace ServiceStack.Webhooks 8 | { 9 | public class MemorySubscriptionStore : ISubscriptionStore 10 | { 11 | private readonly ConcurrentDictionary deliveryResults = new ConcurrentDictionary(); 12 | private readonly ConcurrentDictionary subscriptions = new ConcurrentDictionary(); 13 | 14 | public string Add(WebhookSubscription subscription) 15 | { 16 | Guard.AgainstNull(() => subscription, subscription); 17 | 18 | var id = DataFormats.CreateEntityIdentifier(); 19 | subscription.Id = id; 20 | 21 | subscriptions.TryAdd(id, subscription); 22 | 23 | return id; 24 | } 25 | 26 | public List Find(string userId) 27 | { 28 | return subscriptions 29 | .Where(kvp => kvp.Value.CreatedById.EqualsIgnoreCase(userId)) 30 | .Select(kvp => kvp.Value) 31 | .ToList(); 32 | } 33 | 34 | public WebhookSubscription Get(string userId, string eventName) 35 | { 36 | Guard.AgainstNullOrEmpty(() => eventName, eventName); 37 | 38 | return subscriptions 39 | .Where(kvp => kvp.Value.CreatedById.EqualsIgnoreCase(userId) 40 | && kvp.Value.Event.EqualsIgnoreCase(eventName)) 41 | .Select(kvp => kvp.Value) 42 | .FirstOrDefault(); 43 | } 44 | 45 | public WebhookSubscription Get(string subscriptionId) 46 | { 47 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 48 | 49 | return subscriptions 50 | .Where(kvp => kvp.Value.Id.EqualsIgnoreCase(subscriptionId)) 51 | .Select(kvp => kvp.Value) 52 | .FirstOrDefault(); 53 | } 54 | 55 | public void Update(string subscriptionId, WebhookSubscription subscription) 56 | { 57 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 58 | Guard.AgainstNull(() => subscription, subscription); 59 | 60 | var existing = Get(subscriptionId); 61 | if (existing != null) 62 | { 63 | subscriptions[subscriptionId] = subscription; 64 | } 65 | } 66 | 67 | public void Delete(string subscriptionId) 68 | { 69 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 70 | 71 | var existing = Get(subscriptionId); 72 | if (existing != null) 73 | { 74 | WebhookSubscription subscription; 75 | subscriptions.TryRemove(subscriptionId, out subscription); 76 | } 77 | } 78 | 79 | public List Search(string eventName, bool? isActive) 80 | { 81 | Guard.AgainstNullOrEmpty(() => eventName, eventName); 82 | 83 | return subscriptions 84 | .Where(kvp => kvp.Value.Event.EqualsIgnoreCase(eventName) 85 | && (!isActive.HasValue || isActive.Value == kvp.Value.IsActive)) 86 | .Select(kvp => new SubscriptionRelayConfig 87 | { 88 | Config = kvp.Value.Config, 89 | SubscriptionId = kvp.Value.Id 90 | }) 91 | .ToList(); 92 | } 93 | 94 | public void Add(string subscriptionId, SubscriptionDeliveryResult result) 95 | { 96 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 97 | Guard.AgainstNull(() => result, result); 98 | 99 | var subscription = Get(subscriptionId); 100 | if (subscription != null) 101 | { 102 | deliveryResults.TryAdd(result.Id, result); 103 | } 104 | } 105 | 106 | public List Search(string subscriptionId, int top) 107 | { 108 | Guard.AgainstNullOrEmpty(() => subscriptionId, subscriptionId); 109 | if (top <= 0) 110 | { 111 | throw new ArgumentOutOfRangeException("top"); 112 | } 113 | 114 | return deliveryResults 115 | .Where(kvp => kvp.Value.SubscriptionId.EqualsIgnoreCase(subscriptionId)) 116 | .Select(kvp => kvp.Value) 117 | .OrderByDescending(sub => sub.AttemptedDateUtc) 118 | .Take(top) 119 | .ToList(); 120 | } 121 | 122 | public void Clear() 123 | { 124 | deliveryResults.Clear(); 125 | subscriptions.Clear(); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /src/Webhooks/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("ServiceStack.Webhooks")] 5 | [assembly: AssemblyDescription("Add Webhooks to your ServiceStack services")] 6 | [assembly: Guid("6b1a8528-5b2e-41ef-857b-0bdfa517047a")] -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/CreateSubscription.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using ServiceStack.FluentValidation; 5 | using ServiceStack.Webhooks.Properties; 6 | using ServiceStack.Webhooks.ServiceModel.Types; 7 | 8 | namespace ServiceStack.Webhooks.ServiceModel 9 | { 10 | public interface ISubscriptionConfigValidator : IValidator 11 | { 12 | } 13 | 14 | public class SubscriptionConfigValidator : AbstractValidator, ISubscriptionConfigValidator 15 | { 16 | public SubscriptionConfigValidator() 17 | { 18 | RuleFor(dto => dto.Url).NotEmpty() 19 | .WithMessage(Resources.SubscriptionConfigValidator_InvalidUrl); 20 | RuleFor(dto => dto.Url).IsUrl() 21 | .WithMessage(Resources.SubscriptionConfigValidator_InvalidUrl); 22 | When(dto => dto.ContentType.HasValue(), () => 23 | { 24 | RuleFor(dto => dto.ContentType).Must(dto => dto.EqualsIgnoreCase(MimeTypes.Json)) 25 | .WithMessage(Resources.SubscriptionConfigValidator_UnsupportedContentType); 26 | }); 27 | When(dto => dto.Secret.HasValue(), () => 28 | { 29 | RuleFor(dto => dto.Secret).Matches(DataFormats.Subscriptions.Secret.Expression) 30 | .WithMessage(Resources.SubscriptionConfigValidator_InvalidSecret); 31 | }); 32 | } 33 | } 34 | 35 | public interface ISubscriptionEventsValidator : IValidator> 36 | { 37 | } 38 | 39 | public class SubscriptionEventsValidator : AbstractValidator>, ISubscriptionEventsValidator 40 | { 41 | public SubscriptionEventsValidator() 42 | { 43 | RuleFor(dto => dto).NotNull().WithName("Events") 44 | .WithMessage(Resources.SubscriptionEventsValidator_NoName); 45 | RuleFor(dto => dto).Must(names => names.Any()).WithName("Events") 46 | .WithMessage(Resources.SubscriptionEventsValidator_NoName); 47 | RuleFor(dto => dto).Must(name => name.TrueForAll(x => x.HasValue())).WithName("Events") 48 | .WithMessage(Resources.SubscriptionEventsValidator_EmptyName); 49 | RuleFor(dto => dto).Must(name => name.TrueForAll(x => Regex.IsMatch(x, DataFormats.Subscriptions.Event.Expression))).WithName("Events") 50 | .WithMessage(Resources.SubscriptionEventsValidator_InvalidName); 51 | } 52 | } 53 | 54 | public class CreateSubscriptionValidator : AbstractValidator 55 | { 56 | public CreateSubscriptionValidator(ISubscriptionEventsValidator eventsValidator, ISubscriptionConfigValidator subscriptionConfigValidator) 57 | { 58 | RuleFor(dto => dto.Config).NotNull() 59 | .WithMessage(Resources.CreateSubscriptionValidator_InvalidConfig); 60 | RuleFor(dto => dto.Config) 61 | .SetValidator(subscriptionConfigValidator); 62 | RuleFor(dto => dto.Events).NotNull() 63 | .WithMessage(Resources.CreateSubscriptionValidator_InvalidEvents); 64 | RuleFor(dto => dto.Events) 65 | .SetValidator(eventsValidator); 66 | RuleFor(dto => dto.Name).NotEmpty() 67 | .WithMessage(Resources.CreateSubscriptionValidator_InvalidName); 68 | RuleFor(dto => dto.Name).Matches(DataFormats.Subscriptions.Name.Expression) 69 | .WithMessage(Resources.CreateSubscriptionValidator_InvalidName); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/DeleteSubscription.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | using ServiceStack.Webhooks.Properties; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | public class DeleteSubscriptionValidator : AbstractValidator 7 | { 8 | public DeleteSubscriptionValidator() 9 | { 10 | RuleFor(dto => dto.Id).IsEntityId() 11 | .WithMessage(Resources.GetSubscriptionValidator_InvalidId); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/EntityIdValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using ServiceStack.FluentValidation.Validators; 3 | using ServiceStack.Webhooks.Properties; 4 | 5 | namespace ServiceStack.Webhooks.ServiceModel 6 | { 7 | public interface IEntityIdValidator : IPropertyValidator 8 | { 9 | } 10 | 11 | /// 12 | /// A validator for a entity's identifier property value. 13 | /// 14 | public class EntityIdValidator : PropertyValidator, IEntityIdValidator 15 | { 16 | /// 17 | /// Creates a new instance of the class. 18 | /// 19 | public EntityIdValidator() 20 | : base(Resources.EntityIdValidator_ErrorMessage) 21 | { 22 | } 23 | 24 | /// 25 | /// Whether the property value of the context is valid 26 | /// 27 | protected override bool IsValid(PropertyValidatorContext context) 28 | { 29 | if (context.PropertyValue == null) 30 | { 31 | return false; 32 | } 33 | 34 | var propertyValue = context.PropertyValue.ToString(); 35 | 36 | return Regex.IsMatch(propertyValue, DataFormats.EntityIdentifier.Expression); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/GetSubscription.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | using ServiceStack.Webhooks.Properties; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | public class GetSubscriptionValidator : AbstractValidator 7 | { 8 | public GetSubscriptionValidator() 9 | { 10 | RuleFor(dto => dto.Id).IsEntityId() 11 | .WithMessage(Resources.GetSubscriptionValidator_InvalidId); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/ListSubscriptions.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | 3 | namespace ServiceStack.Webhooks.ServiceModel 4 | { 5 | public class ListSubscriptionsValidator : AbstractValidator 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/SearchSubscriptions.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | using ServiceStack.Webhooks.Properties; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | public class SearchSubscriptionsValidator : AbstractValidator 7 | { 8 | public SearchSubscriptionsValidator() 9 | { 10 | RuleFor(dto => dto.EventName).NotEmpty() 11 | .WithMessage(Resources.SearchSubscriptionsValidator_InvalidEventName); 12 | RuleFor(dto => dto.EventName).Matches(DataFormats.Subscriptions.Event.Expression) 13 | .WithMessage(Resources.SearchSubscriptionsValidator_InvalidEventName); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/UpdateSubscription.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | using ServiceStack.Webhooks.Properties; 3 | 4 | namespace ServiceStack.Webhooks.ServiceModel 5 | { 6 | public class UpdateSubscriptionValidator : AbstractValidator 7 | { 8 | public UpdateSubscriptionValidator() 9 | { 10 | RuleFor(dto => dto.Id).IsEntityId() 11 | .WithMessage(Resources.GetSubscriptionValidator_InvalidId); 12 | When(dto => dto.Url.HasValue(), () => 13 | { 14 | RuleFor(dto => dto.Url).NotEmpty() 15 | .WithMessage(Resources.SubscriptionConfigValidator_InvalidUrl); 16 | RuleFor(dto => dto.Url).IsUrl() 17 | .WithMessage(Resources.SubscriptionConfigValidator_InvalidUrl); 18 | }); 19 | When(dto => dto.ContentType.HasValue(), () => 20 | { 21 | RuleFor(dto => dto.ContentType).Must(dto => dto.EqualsIgnoreCase(MimeTypes.Json)) 22 | .WithMessage(Resources.SubscriptionConfigValidator_UnsupportedContentType); 23 | }); 24 | When(dto => dto.Secret.HasValue(), () => 25 | { 26 | RuleFor(dto => dto.Secret).Matches(DataFormats.Subscriptions.Secret.Expression) 27 | .WithMessage(Resources.SubscriptionConfigValidator_InvalidSecret); 28 | }); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/UpdateSubscriptionHistory.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | using ServiceStack.Webhooks.Properties; 3 | using ServiceStack.Webhooks.ServiceModel.Types; 4 | 5 | namespace ServiceStack.Webhooks.ServiceModel 6 | { 7 | public interface ISubscriptionDeliveryResultValidator : IValidator 8 | { 9 | } 10 | 11 | public class SubscriptionDeliveryResultValidator : AbstractValidator, ISubscriptionDeliveryResultValidator 12 | { 13 | public SubscriptionDeliveryResultValidator() 14 | { 15 | RuleFor(dto => dto.Id).IsEntityId() 16 | .WithMessage(Resources.SubscriptionDeliveryResultValidator_InvalidId); 17 | RuleFor(dto => dto.SubscriptionId).IsEntityId() 18 | .WithMessage(Resources.SubscriptionDeliveryResultValidator_InvalidSubscriptionId); 19 | } 20 | } 21 | 22 | public class UpdateSubscriptionHistoryValidator : AbstractValidator 23 | { 24 | public UpdateSubscriptionHistoryValidator(ISubscriptionDeliveryResultValidator deliveryResultValidator) 25 | { 26 | RuleFor(dto => dto.Results) 27 | .SetCollectionValidator(deliveryResultValidator); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/UrlValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack.FluentValidation.Validators; 3 | using ServiceStack.Webhooks.Properties; 4 | 5 | namespace ServiceStack.Webhooks.ServiceModel 6 | { 7 | internal interface IUrlValidator : IPropertyValidator 8 | { 9 | } 10 | 11 | /// 12 | /// A validator for a URL property value. 13 | /// 14 | internal class UrlValidator : PropertyValidator, IUrlValidator 15 | { 16 | /// 17 | /// Creates a new instance of the class. 18 | /// 19 | public UrlValidator() 20 | : base(Resources.UrlValidator_ErrorMessage) 21 | { 22 | } 23 | 24 | /// 25 | /// Whether the property value of the context is valid 26 | /// 27 | protected override bool IsValid(PropertyValidatorContext context) 28 | { 29 | if (context.PropertyValue == null) 30 | { 31 | return false; 32 | } 33 | 34 | var propertyValue = context.PropertyValue.ToString(); 35 | 36 | return Uri.IsWellFormedUriString(propertyValue, UriKind.Absolute); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Webhooks/ServiceModel/ValidatorExtensions.cs: -------------------------------------------------------------------------------- 1 | using ServiceStack.FluentValidation; 2 | 3 | namespace ServiceStack.Webhooks.ServiceModel 4 | { 5 | /// 6 | /// Extensions for validators 7 | /// 8 | public static class ValidatorExtensions 9 | { 10 | /// 11 | /// Defines a validator that will fail if the property is not a valid URL. 12 | /// 13 | public static IRuleBuilderOptions IsUrl( 14 | this IRuleBuilder ruleBuilder) 15 | { 16 | return ruleBuilder.SetValidator(new UrlValidator()); 17 | } 18 | 19 | /// 20 | /// Defines a validator that will fail if the property is not a valid ID of an entity. 21 | /// 22 | public static IRuleBuilderOptions IsEntityId( 23 | this IRuleBuilder ruleBuilder) 24 | { 25 | return ruleBuilder.SetValidator(new EntityIdValidator()); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Webhooks/WebhookFeature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Funq; 4 | using ServiceStack.Validation; 5 | using ServiceStack.Web; 6 | using ServiceStack.Webhooks.Clients; 7 | using ServiceStack.Webhooks.Relays; 8 | using ServiceStack.Webhooks.Relays.Clients; 9 | using ServiceStack.Webhooks.ServiceInterface; 10 | using ServiceStack.Webhooks.ServiceModel; 11 | 12 | namespace ServiceStack.Webhooks 13 | { 14 | public class WebhookFeature : IPlugin 15 | { 16 | public const string DefaultSubscriberRoles = @"user"; 17 | public const string DefaultRelayRoles = @"service"; 18 | public static readonly string[] RoleDelimiters = {",", ";"}; 19 | 20 | public WebhookFeature() 21 | { 22 | IncludeSubscriptionService = true; 23 | SecureSubscriberRoles = DefaultSubscriberRoles; 24 | SecureRelayRoles = DefaultRelayRoles; 25 | } 26 | 27 | public bool IncludeSubscriptionService { get; set; } 28 | 29 | public string SecureSubscriberRoles { get; set; } 30 | 31 | public string SecureRelayRoles { get; set; } 32 | 33 | public Action PublishEventFilter { get; set; } 34 | 35 | public void Register(IAppHost appHost) 36 | { 37 | var container = appHost.GetContainer(); 38 | 39 | RegisterSubscriptionStore(container); 40 | RegisterSubscriptionService(appHost); 41 | RegisterClient(container); 42 | } 43 | 44 | private void RegisterClient(Container container) 45 | { 46 | if (!container.Exists()) 47 | { 48 | container.RegisterAutoWiredAs(); 49 | container.RegisterAutoWiredAs(); 50 | container.RegisterAutoWiredAs(); 51 | container.RegisterAutoWiredAs(); 52 | container.RegisterAutoWiredAs(); 53 | } 54 | 55 | container.Register(x => new WebhooksClient 56 | { 57 | EventSink = x.Resolve(), 58 | PublishFilter = PublishEventFilter 59 | }).ReusedWithin(ReuseScope.None); 60 | } 61 | 62 | private static void RegisterSubscriptionStore(Container container) 63 | { 64 | if (!container.Exists()) 65 | { 66 | container.RegisterAutoWiredAs(); 67 | } 68 | } 69 | 70 | private void RegisterSubscriptionService(IAppHost appHost) 71 | { 72 | if (IncludeSubscriptionService) 73 | { 74 | var container = appHost.GetContainer(); 75 | appHost.RegisterService(typeof(SubscriptionService)); 76 | 77 | container.RegisterValidators(typeof(WebHookInterfaces).Assembly, typeof(SubscriptionService).Assembly); 78 | container.RegisterAutoWiredAs(); 79 | container.RegisterAutoWiredAs(); 80 | container.RegisterAutoWiredAs(); 81 | 82 | container.RegisterAutoWiredAs().ReusedWithin(ReuseScope.Request); 83 | 84 | if (appHost.Plugins.Exists(plugin => plugin is AuthFeature)) 85 | { 86 | appHost.GlobalRequestFilters.Add(AuthorizeSubscriptionServiceRequests); 87 | } 88 | } 89 | } 90 | 91 | public void AuthorizeSubscriptionServiceRequests(IRequest request, IResponse response, object dto) 92 | { 93 | if (IsSubscriptionService(request.Dto.GetType())) 94 | { 95 | var attribute = new AuthenticateAttribute(); 96 | AsyncHelper.RunSync(() => attribute.ExecuteAsync(request, response, dto)); 97 | 98 | var requiredRoles = GetRequiredRoles(request.Dto); 99 | if (requiredRoles.Length > 0) 100 | { 101 | RequiresAnyRoleAttribute.AssertRequiredRoles(request, requiredRoles); 102 | } 103 | } 104 | } 105 | 106 | private static bool IsSubscriptionService(Type dtoType) 107 | { 108 | return Subscription.AllSubscriptionDtos.Contains(dtoType); 109 | } 110 | 111 | private string[] GetRequiredRoles(object dto) 112 | { 113 | if (dto is SearchSubscriptions || dto is UpdateSubscriptionHistory) 114 | { 115 | return SecureRelayRoles.SafeSplit(RoleDelimiters); 116 | } 117 | 118 | return SecureSubscriberRoles.SafeSplit(RoleDelimiters); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Webhooks/Webhooks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard20;net472 5 | Library 6 | 7 | ServiceStack.Webhooks 8 | ServiceStack.Webhooks 9 | Debug;Release;ReleaseNoTestDeploy 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | True 22 | True 23 | Resources.resx 24 | 25 | 26 | 27 | 28 | PublicResXFileCodeGenerator 29 | Resources.Designer.cs 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Webhooks/Webhooks.v3.ncrunchproject: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /src/Webhooks/WebhooksClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ServiceStack.Logging; 3 | using ServiceStack.Text; 4 | 5 | namespace ServiceStack.Webhooks 6 | { 7 | public class WebhooksClient : IWebhooks 8 | { 9 | private readonly ILog logger = LogManager.GetLogger(typeof(WebhooksClient)); 10 | 11 | public IEventSink EventSink { get; set; } 12 | 13 | public Action PublishFilter { get; set; } 14 | 15 | /// 16 | /// Publishes webhook events to the 17 | /// 18 | public void Publish(string eventName, TDto data) where TDto : class, new() 19 | { 20 | Guard.AgainstNullOrEmpty(() => eventName, eventName); 21 | 22 | var @event = CreateEvent(eventName, data); 23 | if (@event != null) 24 | { 25 | logger.InfoFormat(@"[ServiceStack.Webhooks.WebhooksClient] Publishing webhook event {0}, with data {1}", eventName, @event.ToJson()); 26 | EventSink.Write(@event); 27 | } 28 | } 29 | 30 | protected virtual WebhookEvent CreateEvent(string eventName, TDto data) where TDto : class, new() 31 | { 32 | var @event = new WebhookEvent 33 | { 34 | Id = DataFormats.CreateEntityIdentifier(), 35 | EventName = eventName, 36 | Data = data, 37 | CreatedDateUtc = SystemTime.UtcNow.ToNearestMillisecond(), 38 | Origin = null, 39 | }; 40 | 41 | PublishFilter?.Invoke(@event); 42 | 43 | return @event; 44 | } 45 | } 46 | } --------------------------------------------------------------------------------