diff --git a/Common/HmacDigest.cs b/Common/HmacDigest.cs new file mode 100644 index 0000000..b108e08 --- /dev/null +++ b/Common/HmacDigest.cs @@ -0,0 +1,22 @@ +using System; + +namespace FHIRcastSandbox.Rules { + public class HmacDigest { + public string CreateDigest(string key, string payload) { + var byteKey = System.Text.Encoding.UTF8.GetBytes(key); + using (var hmacHasher = new System.Security.Cryptography.HMACSHA256(byteKey)) { + var digest = hmacHasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload)); + return BitConverter.ToString(digest).Replace("-", "").ToLower(); + } + } + + public string CreateHubSignature(string key, string payload) { + return $"sha256={this.CreateDigest(key, payload)}"; + } + + public bool VerifyHubSignature(string key, string payload, string signature) { + return false; + /* return this.CreateDigest(key, payload) == signature; */ + } + } +} diff --git a/Common/Model/HttpSubscription.cs b/Common/Model/HttpSubscription.cs index 344d198..5544f55 100644 --- a/Common/Model/HttpSubscription.cs +++ b/Common/Model/HttpSubscription.cs @@ -1,5 +1,7 @@ +using System.Net.Http.Headers; using System.Net.Http; using FHIRcastSandbox.Model; +using FHIRcastSandbox.Rules; namespace FHIRcastSandbox.Model.Http { public static class SubscriptionExtensions { @@ -20,5 +22,19 @@ public static HttpContent CreateHttpContent(this Subscription source) { return httpcontent; } } -} + public static class NotificationExtensions { + public static HttpContent CreateHttpContent(this Notification source, string subscriptionSecret = null) { + var str = Newtonsoft.Json.JsonConvert.SerializeObject(source); + var content = new StringContent(str); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + if (subscriptionSecret != null) { + var hubSignature = new HmacDigest().CreateHubSignature(subscriptionSecret, str); + content.Headers.Add("X-Hub-Signature", hubSignature); + } + + return content; + } + } +} diff --git a/Common/Model/Subscriptions.cs b/Common/Model/Subscriptions.cs index 768a974..896b0b2 100644 --- a/Common/Model/Subscriptions.cs +++ b/Common/Model/Subscriptions.cs @@ -113,6 +113,30 @@ public class Notification : ModelBase { public string Id { get; set; } [JsonProperty(PropertyName = "event")] public NotificationEvent Event { get; } = new NotificationEvent(); + + public Notification() { + this.Id = Guid.NewGuid().ToString(); + this.Timestamp = DateTime.Now; + } + + public override bool Equals(object other) { + var notification = other as Notification; + return notification != null && + this.Timestamp == notification.Timestamp && + this.Id == notification.Id && + this.Event.Topic == notification.Event.Topic && + this.Event.Event == notification.Event.Event; + + } + + public override int GetHashCode() { + return new { + this.Timestamp, + this.Id, + this.Event.Topic, + this.Event.Event, + }.GetHashCode(); + } } public class NotificationEvent { diff --git a/Hub/Rules/Notifications.cs b/Hub/Rules/Notifications.cs index d20f411..fcf54bb 100644 --- a/Hub/Rules/Notifications.cs +++ b/Hub/Rules/Notifications.cs @@ -1,9 +1,10 @@ -using FHIRcastSandbox.Model; -using Microsoft.Extensions.Logging; using System.Net.Http.Headers; using System.Net.Http; using System.Threading.Tasks; using System; +using FHIRcastSandbox.Model.Http; +using FHIRcastSandbox.Model; +using Microsoft.Extensions.Logging; namespace FHIRcastSandbox.Rules { public class Notifications : INotifications { @@ -16,10 +17,8 @@ public Notifications(ILogger logger) { public async Task SendNotification(Notification notification, Subscription subscription) { this.logger.LogInformation($"Sending notification {notification} to callback {subscription.Callback}"); - var str = Newtonsoft.Json.JsonConvert.SerializeObject(notification); - var content = new StringContent(str); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); var client = new HttpClient(); + var content = notification.CreateHttpContent(subscription.Secret); var response = await client.PostAsync(subscription.Callback, content); this.logger.LogDebug($"Got response from posting notification:{Environment.NewLine}{response}{Environment.NewLine}{await response.Content.ReadAsStringAsync()}."); diff --git a/Tests/HmacDigestTests.cs b/Tests/HmacDigestTests.cs new file mode 100644 index 0000000..5f76387 --- /dev/null +++ b/Tests/HmacDigestTests.cs @@ -0,0 +1,42 @@ +using System; +using Xunit; + +namespace FHIRcastSandbox.Rules { + public class HmacDigestTests { + [Fact] + public void CreateDigest_KeyAndPayload_CreateCorrectDigest_Test() { + // Arange + var key = "key"; + var payload = "The quick brown fox jumps over the lazy dog"; + + // Act + var result = new HmacDigest().CreateDigest(key, payload); + + // Assert + Assert.Equal("f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", result); + } + + [Fact] + public void CreateHubSignature_KeyAndPayload_CreateSignatureForSha256_Test() { + // Arange + var key = "key"; + var payload = "The quick brown fox jumps over the lazy dog"; + + // Act + var result = new HmacDigest().CreateHubSignature(key, payload); + + // Assert + Assert.Equal("sha256=f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8", result); + } + + [Fact] + public void VerifyHubSignature_CorrectKeyAndPayload_ReturnsTrue_Test() { + // Arrange + var key = "key"; + var payload = "The quick brown fox jumps over the lazy dog"; + + // Act + var result = new HmacDigest().VerifyHubSignature(key, payload, signature); + } + } +} diff --git a/Tests/IntegrationTests.cs b/Tests/IntegrationTests.cs index fecfbd0..b253354 100644 --- a/Tests/IntegrationTests.cs +++ b/Tests/IntegrationTests.cs @@ -5,10 +5,15 @@ using System.Net.Http; using System.Net.Sockets; using System.Threading.Tasks; -using FHIRcastSandbox.Model; +using FHIRcastSandbox.Hubs; using FHIRcastSandbox.Model.Http; +using FHIRcastSandbox.Model; +using FakeItEasy; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Xunit; @@ -18,6 +23,7 @@ public class IntegrationTests : IDisposable { private readonly IWebHost webSubClientServer; private readonly int hubServerPort; private readonly int webSubClientServerPort; + private IClientProxy signalrClientProxy; public IntegrationTests() { this.hubServerPort = this.GetFreePort(); @@ -25,7 +31,7 @@ public IntegrationTests() { Console.WriteLine($"Hub: http://localhost:{this.hubServerPort}"); this.webSubClientServerPort = this.GetFreePort(); - this.webSubClientServer = this.CreateWebSubClientServer(this.webSubClientServerPort); + (this.webSubClientServer, this.signalrClientProxy) = this.CreateWebSubClientServer(this.webSubClientServerPort); Console.WriteLine($"WebSubClient: http://localhost:{this.webSubClientServerPort}"); Task.WaitAll( @@ -34,9 +40,14 @@ public IntegrationTests() { System.Threading.Thread.Sleep(1000); } - private IWebHost CreateWebSubClientServer(int port) { + private (IWebHost, IClientProxy) CreateWebSubClientServer(int port) { + var testSignalrHubContext = A.Fake>(); + var signalrClientProxy = A.Fake(); + A.CallTo(() => testSignalrHubContext.Clients.Clients(A._)) + .Returns(signalrClientProxy); + var contentRoot = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "WebSubClient"); - return FHIRcastSandbox.WebSubClient.Program.CreateWebHostBuilder() + var webHost = FHIRcastSandbox.WebSubClient.Program.CreateWebHostBuilder() .UseKestrel() .UseContentRoot(contentRoot) .UseUrls($"http://localhost:{port}") @@ -45,7 +56,12 @@ private IWebHost CreateWebSubClientServer(int port) { { "Settings:ValidateSubscriptionValidations", "False" }, { "Logging:LogLevel:Default", "Warning" }, })) + .ConfigureTestServices(services => { + services.AddSingleton>(testSignalrHubContext); + }) .Build(); + + return (webHost, signalrClientProxy); } private IWebHost CreateHubServer(int port) { @@ -76,24 +92,35 @@ public async Task ListingSubscriptions_AfterSubscribingToHub_ReturnsSubsription_ var connectionId = "some_client_connection_id"; var subscriptionUrl = $"http://localhost:{this.hubServerPort}/api/hub"; var topic = $"{subscriptionUrl}/{sessionId}"; + var secret = "some_secret"; var events = new[] { "some_event" }; var callback = $"http://localhost:{this.webSubClientServerPort}/callback/{connectionId}"; var subscription = Subscription.CreateNewSubscription(subscriptionUrl, topic, events, callback); + subscription.Secret = secret; var httpContent = subscription.CreateHttpContent(); var clientTestResponse = await new HttpClient().GetAsync(callback); Assert.True(clientTestResponse.IsSuccessStatusCode, $"Could not connect to web sub client: {clientTestResponse}"); + // Subscribe to Hub var subscriptionResponse = await new HttpClient().PostAsync(subscriptionUrl, httpContent); Assert.True(subscriptionResponse.IsSuccessStatusCode, $"Could not subscribe to hub: {subscriptionResponse}"); await Task.Delay(1000); - // Act - var result = await new HttpClient().GetStringAsync(subscriptionUrl); - var subscriptions = JsonConvert.DeserializeObject(result); + // Notify Hub + var notification = new Notification(); + notification.Event.Topic = topic; + notification.Event.Event = events[0]; + var notificationContent = notification.CreateHttpContent(); + var notificationResult = await new HttpClient().PostAsync(topic, notificationContent); + notificationResult.EnsureSuccessStatusCode(); - // Assert - Assert.Single(subscriptions); + // Assert that the notificaiton was sent to the web client + A.CallTo(() => this.signalrClientProxy.SendCoreAsync( + "notification", + A.That.IsSameSequenceAs(new[] { notification }), + A._)) + .MustHaveHappened(); } public void Dispose() { diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 33832a2..512b401 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/WebSubClient/Controllers/CallbackController.cs b/WebSubClient/Controllers/CallbackController.cs index cd8ccc5..a9364b2 100644 --- a/WebSubClient/Controllers/CallbackController.cs +++ b/WebSubClient/Controllers/CallbackController.cs @@ -58,18 +58,10 @@ public IActionResult SubscriptionVerification(string connectionId, [FromQuery] S /// [HttpPost("{subscriptionId}")] public async Task Notification(string subscriptionId, [FromBody] Notification notification) { - //If we do not have an active subscription matching the id then return a notfound error - var clients = this.clientSubscriptions.GetSubscribedClients(notification); + var subscription = this.clientSubscriptions.GetSubscription(subscriptionId); + var key = subscription.Secret; - //var internalModel = new ClientModel() - //{ - // UserIdentifier = notification.Event.Context[0] == null ? "" : notification.Event.Context[0].ToString(), - // PatientIdentifier = notification.Event.Context[1] == null ? "" : notification.Event.Context[1].ToString(), - // PatientIdIssuer = notification.Event.Context[2] == null ? "" : notification.Event.Context[2].ToString(), - // AccessionNumber = notification.Event.Context[3] == null ? "" : notification.Event.Context[3].ToString(), - // AccessionNumberGroup = notification.Event.Context[4] == null ? "" : notification.Event.Context[4].ToString(), - // StudyId = notification.Event.Context[5] == null ? "" : notification.Event.Context[5].ToString(), - //}; + var clients = this.clientSubscriptions.GetSubscribedClients(notification); await this.webSubClientHubContext.Clients.Clients(clients) .SendAsync("notification", notification); diff --git a/WebSubClient/Controllers/WebSubClientController.cs b/WebSubClient/Controllers/WebSubClientController.cs index 09a988d..246e670 100644 --- a/WebSubClient/Controllers/WebSubClientController.cs +++ b/WebSubClient/Controllers/WebSubClientController.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; +using FHIRcastSandbox.Model.Http; using FHIRcastSandbox.Model; using FHIRcastSandbox.WebSubClient.Rules; using Microsoft.AspNetCore.Mvc; @@ -43,11 +44,7 @@ public WebSubClientController(ILogger logger, ClientSubs [HttpPost("notify")] public async Task Post([FromForm] ClientModel model) { var httpClient = new HttpClient(); - var notification = new Notification - { - Id = Guid.NewGuid().ToString(), - Timestamp = DateTime.Now, - }; + var notification = new Notification(); notification.Event.Context = new[] { new { model.AccessionNumber, @@ -61,7 +58,8 @@ public async Task Post([FromForm] ClientModel model) { notification.Event.Topic = model.Topic; notification.Event.Event = model.Event; - var response = await httpClient.PostAsync(model.Topic, new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json")); + var content = notification.CreateHttpContent(); + var response = await httpClient.PostAsync(model.Topic, content); response.EnsureSuccessStatusCode();