diff --git a/src/Api/Controllers/PushController.cs b/src/Api/Controllers/PushController.cs index d6b843bd21..9ae0155e34 100644 --- a/src/Api/Controllers/PushController.cs +++ b/src/Api/Controllers/PushController.cs @@ -15,12 +15,14 @@ namespace Bit.Api.Controllers public class PushController : Controller { private readonly IPushRegistrationService _pushRegistrationService; + private readonly IPushNotificationService _pushNotificationService; private readonly IHostingEnvironment _environment; private readonly CurrentContext _currentContext; private readonly GlobalSettings _globalSettings; public PushController( IPushRegistrationService pushRegistrationService, + IPushNotificationService pushNotificationService, IHostingEnvironment environment, CurrentContext currentContext, GlobalSettings globalSettings) @@ -28,6 +30,7 @@ namespace Bit.Api.Controllers _currentContext = currentContext; _environment = environment; _pushRegistrationService = pushRegistrationService; + _pushNotificationService = pushNotificationService; _globalSettings = globalSettings; } @@ -62,8 +65,30 @@ namespace Bit.Api.Controllers model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId)); } + [HttpPost("send")] + public async Task PostSend(PushSendRequestModel model) + { + CheckUsage(); + + if(!string.IsNullOrWhiteSpace(model.UserId)) + { + await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.OrganizationId), + model.Type.Value, model.Payload, Prefix(model.Identifier)); + } + else if(!string.IsNullOrWhiteSpace(model.OrganizationId)) + { + await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId), + model.Type.Value, model.Payload, Prefix(model.Identifier)); + } + } + private string Prefix(string value) { + if(string.IsNullOrWhiteSpace(value)) + { + return null; + } + return $"{_currentContext.InstallationId.Value}_{value}"; } diff --git a/src/Core/Models/Api/Request/PushSendRequestModel.cs b/src/Core/Models/Api/Request/PushSendRequestModel.cs new file mode 100644 index 0000000000..e58fb8fa10 --- /dev/null +++ b/src/Core/Models/Api/Request/PushSendRequestModel.cs @@ -0,0 +1,25 @@ +using Bit.Core.Enums; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; + +namespace Bit.Core.Models.Api +{ + public class PushSendRequestModel : IValidatableObject + { + public string UserId { get; set; } + public string OrganizationId { get; set; } + public string Identifier { get; set; } + [Required] + public PushType? Type { get; set; } + [Required] + public object Payload { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if(string.IsNullOrWhiteSpace(UserId) && string.IsNullOrWhiteSpace(OrganizationId)) + { + yield return new ValidationResult($"{nameof(UserId)} or {nameof(OrganizationId)} is required."); + } + } + } +} diff --git a/src/Core/Services/IPushNotificationService.cs b/src/Core/Services/IPushNotificationService.cs index 18786a71d2..131bca808f 100644 --- a/src/Core/Services/IPushNotificationService.cs +++ b/src/Core/Services/IPushNotificationService.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Bit.Core.Models.Table; +using Bit.Core.Enums; namespace Bit.Core.Services { @@ -16,5 +17,7 @@ namespace Bit.Core.Services Task PushSyncVaultAsync(Guid userId); Task PushSyncOrgKeysAsync(Guid userId); Task PushSyncSettingsAsync(Guid userId); + Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier); + Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier); } } diff --git a/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs b/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs new file mode 100644 index 0000000000..fe855d48e1 --- /dev/null +++ b/src/Core/Services/Implementations/BaseRelayPushNotificationService.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Net.Http; +using Newtonsoft.Json; +using System.Text; +using System; +using Newtonsoft.Json.Linq; +using Bit.Core.Utilities; +using System.Net; + +namespace Bit.Core.Services +{ + public abstract class BaseRelayPushNotificationService + { + private dynamic _decodedToken; + private DateTime? _nextAuthAttempt = null; + + public BaseRelayPushNotificationService( + GlobalSettings globalSettings) + { + GlobalSettings = globalSettings; + + PushClient = new HttpClient + { + BaseAddress = new Uri(globalSettings.PushRelayBaseUri) + }; + + IdentityClient = new HttpClient + { + BaseAddress = new Uri(globalSettings.Installation.IdentityUri) + }; + } + + protected HttpClient PushClient { get; private set; } + protected HttpClient IdentityClient { get; private set; } + protected GlobalSettings GlobalSettings { get; private set; } + protected string AccessToken { get; private set; } + + protected async Task HandleTokenStateAsync() + { + if(_nextAuthAttempt.HasValue && DateTime.UtcNow > _nextAuthAttempt.Value) + { + return false; + } + _nextAuthAttempt = null; + + if(!string.IsNullOrWhiteSpace(AccessToken) && !TokenExpired()) + { + return true; + } + + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(IdentityClient.BaseAddress, "connect/token"), + Content = new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "client_credentials" }, + { "scope", "api.push" }, + { "client_id", $"installation.{GlobalSettings.Installation.Id}" }, + { "client_secret", $"{GlobalSettings.Installation.Key}" } + }) + }; + + var response = await IdentityClient.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + if(response.StatusCode == HttpStatusCode.BadRequest) + { + _nextAuthAttempt = DateTime.UtcNow.AddDays(1); + } + + return false; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + dynamic tokenResponse = JsonConvert.DeserializeObject(responseContent); + AccessToken = (string)tokenResponse.access_token; + return true; + } + + protected class TokenHttpRequestMessage : HttpRequestMessage + { + public TokenHttpRequestMessage(string token) + { + Headers.Add("Authorization", $"Bearer {token}"); + } + + public TokenHttpRequestMessage(object requestObject, string token) + : this(token) + { + var stringContent = JsonConvert.SerializeObject(requestObject); + Content = new StringContent(stringContent, Encoding.UTF8, "application/json"); + } + } + + protected bool TokenExpired() + { + var decoded = DecodeToken(); + var exp = decoded?["exp"]; + if(exp == null) + { + throw new InvalidOperationException("No exp in token."); + } + + var expiration = CoreHelpers.FromEpocMilliseconds(1000 * exp.Value()); + return DateTime.UtcNow < expiration; + } + + protected JObject DecodeToken() + { + if(_decodedToken != null) + { + return _decodedToken; + } + + if(AccessToken == null) + { + throw new InvalidOperationException($"{nameof(AccessToken)} not found."); + } + + var parts = AccessToken.Split('.'); + if(parts.Length != 3) + { + throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts"); + } + + var decodedBytes = CoreHelpers.Base64UrlDecode(parts[1]); + if(decodedBytes == null || decodedBytes.Length < 1) + { + throw new InvalidOperationException($"{nameof(AccessToken)} must have 3 parts"); + } + + _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length)); + return _decodedToken; + } + } +} diff --git a/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs b/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs index 4b335953db..9bf1229389 100644 --- a/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs +++ b/src/Core/Services/Implementations/NotificationHubPushNotificationService.cs @@ -126,26 +126,43 @@ namespace Bit.Core.Services private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext) { - var tag = BuildTag($"template:payload_userId:{userId}", excludeCurrentContext); - await SendPayloadAsync(tag, type, payload); + await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext)); } private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext) { - var tag = BuildTag($"template:payload && organizationId:{orgId}", excludeCurrentContext); + await SendPayloadToUserAsync(orgId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext)); + } + + public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier) + { + var tag = BuildTag($"template:payload_userId:{userId}", identifier); await SendPayloadAsync(tag, type, payload); } - private string BuildTag(string tag, bool excludeCurrentContext) + public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier) { - if(excludeCurrentContext) + var tag = BuildTag($"template:payload && organizationId:{orgId}", identifier); + await SendPayloadAsync(tag, type, payload); + } + + private string GetContextIdentifier(bool excludeCurrentContext) + { + if(!excludeCurrentContext) { - var currentContext = _httpContextAccessor?.HttpContext?. + return null; + } + + var currentContext = _httpContextAccessor?.HttpContext?. RequestServices.GetService(typeof(CurrentContext)) as CurrentContext; - if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier)) - { - tag += $" && !deviceIdentifier:{currentContext.DeviceIdentifier}"; - } + return currentContext?.DeviceIdentifier; + } + + private string BuildTag(string tag, string identifier) + { + if(!string.IsNullOrWhiteSpace(identifier)) + { + tag += $" && !deviceIdentifier:{identifier}"; } return $"({tag})"; diff --git a/src/Core/Services/Implementations/RelayPushNotificationService.cs b/src/Core/Services/Implementations/RelayPushNotificationService.cs new file mode 100644 index 0000000000..f9313fbeb2 --- /dev/null +++ b/src/Core/Services/Implementations/RelayPushNotificationService.cs @@ -0,0 +1,193 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Table; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Http; +using Bit.Core.Models; +using System.Net.Http; +using Bit.Core.Models.Api; + +namespace Bit.Core.Services +{ + public class RelayPushNotificationService : BaseRelayPushNotificationService, IPushNotificationService + { + private readonly HttpClient _client; + private readonly IHttpContextAccessor _httpContextAccessor; + + public RelayPushNotificationService( + GlobalSettings globalSettings, + IHttpContextAccessor httpContextAccessor) + : base(globalSettings) + { + _httpContextAccessor = httpContextAccessor; + } + + public async Task PushSyncCipherCreateAsync(Cipher cipher) + { + await PushCipherAsync(cipher, PushType.SyncCipherCreate); + } + + public async Task PushSyncCipherUpdateAsync(Cipher cipher) + { + await PushCipherAsync(cipher, PushType.SyncCipherUpdate); + } + + public async Task PushSyncCipherDeleteAsync(Cipher cipher) + { + await PushCipherAsync(cipher, PushType.SyncLoginDelete); + } + + private async Task PushCipherAsync(Cipher cipher, PushType type) + { + if(cipher.OrganizationId.HasValue) + { + // We cannot send org pushes since access logic is much more complicated than just the fact that they belong + // to the organization. Potentially we could blindly send to just users that have the access all permission + // device registration needs to be more granular to handle that appropriately. A more brute force approach could + // me to send "full sync" push to all org users, but that has the potential to DDOS the API in bursts. + + // await SendPayloadToOrganizationAsync(cipher.OrganizationId.Value, type, message, true); + } + else if(cipher.UserId.HasValue) + { + var message = new SyncCipherPushNotification + { + Id = cipher.Id, + UserId = cipher.UserId, + OrganizationId = cipher.OrganizationId, + RevisionDate = cipher.RevisionDate, + }; + + await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true); + } + } + + public async Task PushSyncFolderCreateAsync(Folder folder) + { + await PushFolderAsync(folder, PushType.SyncFolderCreate); + } + + public async Task PushSyncFolderUpdateAsync(Folder folder) + { + await PushFolderAsync(folder, PushType.SyncFolderUpdate); + } + + public async Task PushSyncFolderDeleteAsync(Folder folder) + { + await PushFolderAsync(folder, PushType.SyncFolderDelete); + } + + private async Task PushFolderAsync(Folder folder, PushType type) + { + var message = new SyncFolderPushNotification + { + Id = folder.Id, + UserId = folder.UserId, + RevisionDate = folder.RevisionDate + }; + + await SendPayloadToUserAsync(folder.UserId, type, message, true); + } + + public async Task PushSyncCiphersAsync(Guid userId) + { + await PushSyncUserAsync(userId, PushType.SyncCiphers); + } + + public async Task PushSyncVaultAsync(Guid userId) + { + await PushSyncUserAsync(userId, PushType.SyncVault); + } + + public async Task PushSyncOrgKeysAsync(Guid userId) + { + await PushSyncUserAsync(userId, PushType.SyncOrgKeys); + } + + public async Task PushSyncSettingsAsync(Guid userId) + { + await PushSyncUserAsync(userId, PushType.SyncSettings); + } + + private async Task PushSyncUserAsync(Guid userId, PushType type) + { + var message = new SyncUserPushNotification + { + UserId = userId, + Date = DateTime.UtcNow + }; + + await SendPayloadToUserAsync(userId, type, message, false); + } + + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext) + { + var request = new PushSendRequestModel + { + UserId = userId.ToString(), + Type = type, + Payload = payload + }; + + if(excludeCurrentContext) + { + ExcludeCurrentContext(request); + } + + await SendAsync(request); + } + + private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool excludeCurrentContext) + { + var request = new PushSendRequestModel + { + OrganizationId = orgId.ToString(), + Type = type, + Payload = payload + }; + + if(excludeCurrentContext) + { + ExcludeCurrentContext(request); + } + + await SendAsync(request); + } + + private async Task SendAsync(PushSendRequestModel requestModel) + { + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse) + { + return; + } + + var message = new TokenHttpRequestMessage(requestModel, AccessToken) + { + Method = HttpMethod.Post, + RequestUri = new Uri(PushClient.BaseAddress, "send") + }; + await PushClient.SendAsync(message); + } + + private void ExcludeCurrentContext(PushSendRequestModel request) + { + var currentContext = _httpContextAccessor?.HttpContext?. + RequestServices.GetService(typeof(CurrentContext)) as CurrentContext; + if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier)) + { + request.Identifier = currentContext.DeviceIdentifier; + } + } + + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier) + { + throw new NotImplementedException(); + } + + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Core/Services/Implementations/RelayPushRegistrationService.cs b/src/Core/Services/Implementations/RelayPushRegistrationService.cs index cc15f69a1e..8036410ba0 100644 --- a/src/Core/Services/Implementations/RelayPushRegistrationService.cs +++ b/src/Core/Services/Implementations/RelayPushRegistrationService.cs @@ -1,42 +1,21 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Net.Http; -using Newtonsoft.Json; -using System.Text; using System; -using Newtonsoft.Json.Linq; -using Bit.Core.Utilities; -using System.Net; using Bit.Core.Models.Api; using Bit.Core.Enums; using System.Linq; namespace Bit.Core.Services { - public class RelayPushRegistrationService : IPushRegistrationService + public class RelayPushRegistrationService : BaseRelayPushNotificationService, IPushRegistrationService { - private readonly HttpClient _pushClient; - private readonly HttpClient _identityClient; - private readonly GlobalSettings _globalSettings; - private string _accessToken; private dynamic _decodedToken; private DateTime? _nextAuthAttempt = null; - public RelayPushRegistrationService( - GlobalSettings globalSettings) - { - _globalSettings = globalSettings; - - _pushClient = new HttpClient - { - BaseAddress = new Uri(globalSettings.PushRelayBaseUri) - }; - - _identityClient = new HttpClient - { - BaseAddress = new Uri(globalSettings.Installation.IdentityUri) - }; - } + public RelayPushRegistrationService(GlobalSettings globalSettings) + : base(globalSettings) + { } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, string identifier, DeviceType type) @@ -56,12 +35,12 @@ namespace Bit.Core.Services UserId = userId }; - var message = new TokenHttpRequestMessage(requestModel, _accessToken) + var message = new TokenHttpRequestMessage(requestModel, AccessToken) { Method = HttpMethod.Post, - RequestUri = new Uri(_pushClient.BaseAddress, "register") + RequestUri = new Uri(PushClient.BaseAddress, "register") }; - await _pushClient.SendAsync(message); + await PushClient.SendAsync(message); } public async Task DeleteRegistrationAsync(string deviceId) @@ -72,12 +51,12 @@ namespace Bit.Core.Services return; } - var message = new TokenHttpRequestMessage(_accessToken) + var message = new TokenHttpRequestMessage(AccessToken) { Method = HttpMethod.Delete, - RequestUri = new Uri(_pushClient.BaseAddress, deviceId) + RequestUri = new Uri(PushClient.BaseAddress, deviceId) }; - await _pushClient.SendAsync(message); + await PushClient.SendAsync(message); } public async Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) @@ -94,12 +73,12 @@ namespace Bit.Core.Services } var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); - var message = new TokenHttpRequestMessage(requestModel, _accessToken) + var message = new TokenHttpRequestMessage(requestModel, AccessToken) { Method = HttpMethod.Put, - RequestUri = new Uri(_pushClient.BaseAddress, "add-organization") + RequestUri = new Uri(PushClient.BaseAddress, "add-organization") }; - await _pushClient.SendAsync(message); + await PushClient.SendAsync(message); } public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) @@ -116,138 +95,12 @@ namespace Bit.Core.Services } var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); - var message = new TokenHttpRequestMessage(requestModel, _accessToken) + var message = new TokenHttpRequestMessage(requestModel, AccessToken) { Method = HttpMethod.Put, - RequestUri = new Uri(_pushClient.BaseAddress, "delete-organization") + RequestUri = new Uri(PushClient.BaseAddress, "delete-organization") }; - await _pushClient.SendAsync(message); - } - - private async Task HandleTokenStateAsync() - { - if(_nextAuthAttempt.HasValue && DateTime.UtcNow > _nextAuthAttempt.Value) - { - return false; - } - _nextAuthAttempt = null; - - if(!string.IsNullOrWhiteSpace(_accessToken) && !TokenExpired()) - { - return true; - } - - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(_identityClient.BaseAddress, "connect/token"), - Content = new FormUrlEncodedContent(new Dictionary - { - { "grant_type", "client_credentials" }, - { "scope", "api.push" }, - { "client_id", $"installation.{_globalSettings.Installation.Id}" }, - { "client_secret", $"{_globalSettings.Installation.Key}" } - }) - }; - - var response = await _identityClient.SendAsync(requestMessage); - if(!response.IsSuccessStatusCode) - { - if(response.StatusCode == HttpStatusCode.BadRequest) - { - _nextAuthAttempt = DateTime.UtcNow.AddDays(1); - } - - return false; - } - - var responseContent = await response.Content.ReadAsStringAsync(); - dynamic tokenResponse = JsonConvert.DeserializeObject(responseContent); - _accessToken = (string)tokenResponse.access_token; - return true; - } - - public class TokenHttpRequestMessage : HttpRequestMessage - { - public TokenHttpRequestMessage(string token) - { - Headers.Add("Authorization", $"Bearer {token}"); - } - - public TokenHttpRequestMessage(object requestObject, string token) - : this(token) - { - var stringContent = JsonConvert.SerializeObject(requestObject); - Content = new StringContent(stringContent, Encoding.UTF8, "application/json"); - } - } - - public bool TokenExpired() - { - var decoded = DecodeToken(); - var exp = decoded?["exp"]; - if(exp == null) - { - throw new InvalidOperationException("No exp in token."); - } - - var expiration = CoreHelpers.FromEpocMilliseconds(1000 * exp.Value()); - return DateTime.UtcNow < expiration; - } - - private JObject DecodeToken() - { - if(_decodedToken != null) - { - return _decodedToken; - } - - if(_accessToken == null) - { - throw new InvalidOperationException($"{nameof(_accessToken)} not found."); - } - - var parts = _accessToken.Split('.'); - if(parts.Length != 3) - { - throw new InvalidOperationException($"{nameof(_accessToken)} must have 3 parts"); - } - - var decodedBytes = Base64UrlDecode(parts[1]); - if(decodedBytes == null || decodedBytes.Length < 1) - { - throw new InvalidOperationException($"{nameof(_accessToken)} must have 3 parts"); - } - - _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length)); - return _decodedToken; - } - - private byte[] Base64UrlDecode(string input) - { - var output = input; - // 62nd char of encoding - output = output.Replace('-', '+'); - // 63rd char of encoding - output = output.Replace('_', '/'); - // Pad with trailing '='s - switch(output.Length % 4) - { - case 0: - // No pad chars in this case - break; - case 2: - // Two pad chars - output += "=="; break; - case 3: - // One pad char - output += "="; break; - default: - throw new InvalidOperationException("Illegal base64url string!"); - } - - // Standard base64 decoder - return Convert.FromBase64String(output); + await PushClient.SendAsync(message); } } } diff --git a/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs b/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs index 77325d8d92..89d305e624 100644 --- a/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Table; namespace Bit.Core.Services @@ -55,5 +56,15 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier) + { + return Task.FromResult(0); + } + + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier) + { + return Task.FromResult(0); + } } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 2d23f6a1b0..98c7e36b72 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -289,5 +289,41 @@ namespace Bit.Core.Utilities return true; } + + public static string Base64UrlEncode(byte[] input) + { + var output = Convert.ToBase64String(input) + .Replace('+', '-') + .Replace('/', '_') + .Replace("=", string.Empty); + return output; + } + + public static byte[] Base64UrlDecode(string input) + { + var output = input; + // 62nd char of encoding + output = output.Replace('-', '+'); + // 63rd char of encoding + output = output.Replace('_', '/'); + // Pad with trailing '='s + switch(output.Length % 4) + { + case 0: + // No pad chars in this case + break; + case 2: + // Two pad chars + output += "=="; break; + case 3: + // One pad char + output += "="; break; + default: + throw new InvalidOperationException("Illegal base64url string!"); + } + + // Standard base64 decoder + return Convert.FromBase64String(output); + } } } diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index b1b09b2748..c4de428d52 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -71,21 +71,26 @@ namespace Bit.Core.Utilities services.AddSingleton(); } -#if NET461 - if(globalSettings.SelfHosted) + if(globalSettings.SelfHosted && + CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && + globalSettings.Installation?.Id != null && + CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) { - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } - else +#if NET461 + else if(!globalSettings.SelfHosted) { services.AddSingleton(); services.AddSingleton(); } -#else - services.AddSingleton(); - services.AddSingleton(); #endif + else + { + services.AddSingleton(); + services.AddSingleton(); + } if(CoreHelpers.SettingHasValue(globalSettings.Storage.ConnectionString)) {