mirror of
https://github.com/bitwarden/server
synced 2025-12-25 12:43:14 +00:00
[PM-16787] Web push enablement for server (#5395)
* Allow for binning of comb IDs by date and value * Introduce notification hub pool * Replace device type sharding with comb + range sharding * Fix proxy interface * Use enumerable services for multiServiceNotificationHub * Fix push interface usage * Fix push notification service dependencies * Fix push notification keys * Fixup documentation * Remove deprecated settings * Fix tests * PascalCase method names * Remove unused request model properties * Remove unused setting * Improve DateFromComb precision * Prefer readonly service enumerable * Pascal case template holes * Name TryParse methods TryParse * Apply suggestions from code review Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Include preferred push technology in config response SignalR will be the fallback, but clients should attempt web push first if offered and available to the client. * Register web push devices * Working signing and content encrypting * update to RFC-8291 and RFC-8188 * Notification hub is now working, no need to create our own * Fix body * Flip Success Check * use nifty json attribute * Remove vapid private key This is only needed to encrypt data for transmission along webpush -- it's handled by NotificationHub for us * Add web push feature flag to control config response * Update src/Core/NotificationHub/NotificationHubConnection.cs Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Update src/Core/NotificationHub/NotificationHubConnection.cs Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * fixup! Update src/Core/NotificationHub/NotificationHubConnection.cs * Move to platform ownership * Remove debugging extension * Remove unused dependencies * Set json content directly * Name web push registration data * Fix FCM type typo * Determine specific feature flag from set of flags * Fixup merged tests * Fixup tests * Code quality suggestions * Fix merged tests * Fix test --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
@@ -1,82 +1,131 @@
|
||||
using Bit.Core.Enums;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.NotificationHub;
|
||||
|
||||
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
{
|
||||
private static readonly JsonSerializerOptions webPushSerializationOptions = new()
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly INotificationHubPool _notificationHubPool;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||
|
||||
public NotificationHubPushRegistrationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
INotificationHubPool notificationHubPool)
|
||||
INotificationHubPool notificationHubPool,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<NotificationHubPushRegistrationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_notificationHubPool = notificationHubPool;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||
public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId,
|
||||
string identifier, DeviceType type, IEnumerable<string> organizationIds, Guid installationId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pushToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var orgIds = organizationIds.ToList();
|
||||
var clientType = DeviceTypes.ToClientType(type);
|
||||
var installation = new Installation
|
||||
{
|
||||
InstallationId = deviceId,
|
||||
PushChannel = pushToken,
|
||||
PushChannel = data.Token,
|
||||
Tags = new List<string>
|
||||
{
|
||||
$"userId:{userId}",
|
||||
$"clientType:{clientType}"
|
||||
}.Concat(orgIds.Select(organizationId => $"organizationId:{organizationId}")).ToList(),
|
||||
Templates = new Dictionary<string, InstallationTemplate>()
|
||||
};
|
||||
|
||||
var clientType = DeviceTypes.ToClientType(type);
|
||||
|
||||
installation.Tags = new List<string> { $"userId:{userId}", $"clientType:{clientType}" };
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
installation.Tags.Add("deviceIdentifier:" + identifier);
|
||||
}
|
||||
|
||||
var organizationIdsList = organizationIds.ToList();
|
||||
foreach (var organizationId in organizationIdsList)
|
||||
{
|
||||
installation.Tags.Add($"organizationId:{organizationId}");
|
||||
}
|
||||
|
||||
if (installationId != Guid.Empty)
|
||||
{
|
||||
installation.Tags.Add($"installationId:{installationId}");
|
||||
}
|
||||
|
||||
string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null;
|
||||
if (data.Token != null)
|
||||
{
|
||||
await CreateOrUpdateMobileRegistrationAsync(installation, userId, identifier, clientType, orgIds, type, installationId);
|
||||
}
|
||||
else if (data.WebPush != null)
|
||||
{
|
||||
await CreateOrUpdateWebRegistrationAsync(data.WebPush.Value.Endpoint, data.WebPush.Value.P256dh, data.WebPush.Value.Auth, installation, userId, identifier, clientType, orgIds, installationId);
|
||||
}
|
||||
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId,
|
||||
string identifier, ClientType clientType, List<string> organizationIds, DeviceType type, Guid installationId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(installation.PushChannel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case DeviceType.Android:
|
||||
payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}";
|
||||
messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," +
|
||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}";
|
||||
installation.Templates.Add(BuildInstallationTemplate("payload",
|
||||
"{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("message",
|
||||
"{\"message\":{\"data\":{\"type\":\"$(type)\"}," +
|
||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("badgeMessage",
|
||||
"{\"message\":{\"data\":{\"type\":\"$(type)\"}," +
|
||||
"\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Platform = NotificationPlatform.FcmV1;
|
||||
break;
|
||||
case DeviceType.iOS:
|
||||
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," +
|
||||
"\"aps\":{\"content-available\":1}}";
|
||||
messageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}";
|
||||
badgeMessageTemplate = "{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}";
|
||||
|
||||
installation.Templates.Add(BuildInstallationTemplate("payload",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," +
|
||||
"\"aps\":{\"content-available\":1}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("message",
|
||||
"{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"aps\":{\"alert\":\"$(message)\",\"badge\":null,\"content-available\":1}}", userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("badgeMessage",
|
||||
"{\"data\":{\"type\":\"#(type)\"}," +
|
||||
"\"aps\":{\"alert\":\"$(message)\",\"badge\":\"#(badge)\",\"content-available\":1}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Platform = NotificationPlatform.Apns;
|
||||
break;
|
||||
case DeviceType.AndroidAmazon:
|
||||
payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}";
|
||||
messageTemplate = "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}";
|
||||
installation.Templates.Add(BuildInstallationTemplate("payload",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("message",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("badgeMessage",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
|
||||
installation.Platform = NotificationPlatform.Adm;
|
||||
break;
|
||||
@@ -84,28 +133,62 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
break;
|
||||
}
|
||||
|
||||
BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType,
|
||||
organizationIdsList, installationId);
|
||||
BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType,
|
||||
organizationIdsList, installationId);
|
||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||
userId, identifier, clientType, organizationIdsList, installationId);
|
||||
|
||||
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
}
|
||||
await ClientFor(GetComb(installation.InstallationId)).CreateOrUpdateInstallationAsync(installation);
|
||||
}
|
||||
|
||||
private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody,
|
||||
string userId, string identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
|
||||
private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId,
|
||||
string identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
|
||||
{
|
||||
if (templateBody == null)
|
||||
// The Azure SDK is currently lacking support for web push registrations.
|
||||
// We need to use the REST API directly.
|
||||
|
||||
if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(p256dh) || string.IsNullOrWhiteSpace(auth))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
installation.Templates.Add(BuildInstallationTemplate("payload",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("message",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
installation.Templates.Add(BuildInstallationTemplate("badgeMessage",
|
||||
"{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}",
|
||||
userId, identifier, clientType, organizationIds, installationId));
|
||||
|
||||
var content = new
|
||||
{
|
||||
installationId = installation.InstallationId,
|
||||
pushChannel = new
|
||||
{
|
||||
endpoint,
|
||||
p256dh,
|
||||
auth
|
||||
},
|
||||
platform = "browser",
|
||||
tags = installation.Tags,
|
||||
templates = installation.Templates
|
||||
};
|
||||
|
||||
var client = _httpClientFactory.CreateClient("NotificationHub");
|
||||
var request = ConnectionFor(GetComb(installation.InstallationId)).CreateRequest(HttpMethod.Put, $"installations/{installation.InstallationId}");
|
||||
request.Content = JsonContent.Create(content, new MediaTypeHeaderValue("application/json"), webPushSerializationOptions);
|
||||
var response = await client.SendAsync(request);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Web push registration failed: {Response}", body);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Web push registration success: {Response}", body);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyValuePair<string, InstallationTemplate> BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody,
|
||||
string userId, string identifier, ClientType clientType, List<string> organizationIds, Guid installationId)
|
||||
{
|
||||
var fullTemplateId = $"template:{templateId}";
|
||||
|
||||
var template = new InstallationTemplate
|
||||
@@ -132,7 +215,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
template.Tags.Add($"installationId:{installationId}");
|
||||
}
|
||||
|
||||
installation.Templates.Add(fullTemplateId, template);
|
||||
return new KeyValuePair<string, InstallationTemplate>(fullTemplateId, template);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId)
|
||||
@@ -213,6 +296,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
return _notificationHubPool.ClientFor(deviceId);
|
||||
}
|
||||
|
||||
private NotificationHubConnection ConnectionFor(Guid deviceId)
|
||||
{
|
||||
return _notificationHubPool.ConnectionFor(deviceId);
|
||||
}
|
||||
|
||||
private Guid GetComb(string deviceId)
|
||||
{
|
||||
var deviceIdString = deviceId;
|
||||
|
||||
Reference in New Issue
Block a user