mirror of
https://github.com/bitwarden/server.git
synced 2025-04-07 05:58:13 -05:00
push notification relay service and relay send api
This commit is contained in:
parent
0f37920de2
commit
6fe5e3b849
@ -15,12 +15,14 @@ namespace Bit.Api.Controllers
|
|||||||
public class PushController : Controller
|
public class PushController : Controller
|
||||||
{
|
{
|
||||||
private readonly IPushRegistrationService _pushRegistrationService;
|
private readonly IPushRegistrationService _pushRegistrationService;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly IHostingEnvironment _environment;
|
private readonly IHostingEnvironment _environment;
|
||||||
private readonly CurrentContext _currentContext;
|
private readonly CurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
public PushController(
|
public PushController(
|
||||||
IPushRegistrationService pushRegistrationService,
|
IPushRegistrationService pushRegistrationService,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
IHostingEnvironment environment,
|
IHostingEnvironment environment,
|
||||||
CurrentContext currentContext,
|
CurrentContext currentContext,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
@ -28,6 +30,7 @@ namespace Bit.Api.Controllers
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
_pushRegistrationService = pushRegistrationService;
|
_pushRegistrationService = pushRegistrationService;
|
||||||
|
_pushNotificationService = pushNotificationService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,8 +65,30 @@ namespace Bit.Api.Controllers
|
|||||||
model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId));
|
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)
|
private string Prefix(string value)
|
||||||
{
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return $"{_currentContext.InstallationId.Value}_{value}";
|
return $"{_currentContext.InstallationId.Value}_{value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
25
src/Core/Models/Api/Request/PushSendRequestModel.cs
Normal file
25
src/Core/Models/Api/Request/PushSendRequestModel.cs
Normal file
@ -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<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(UserId) && string.IsNullOrWhiteSpace(OrganizationId))
|
||||||
|
{
|
||||||
|
yield return new ValidationResult($"{nameof(UserId)} or {nameof(OrganizationId)} is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -16,5 +17,7 @@ namespace Bit.Core.Services
|
|||||||
Task PushSyncVaultAsync(Guid userId);
|
Task PushSyncVaultAsync(Guid userId);
|
||||||
Task PushSyncOrgKeysAsync(Guid userId);
|
Task PushSyncOrgKeysAsync(Guid userId);
|
||||||
Task PushSyncSettingsAsync(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<bool> 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<string, string>
|
||||||
|
{
|
||||||
|
{ "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<long>());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -126,26 +126,43 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
|
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
|
||||||
{
|
{
|
||||||
var tag = BuildTag($"template:payload_userId:{userId}", excludeCurrentContext);
|
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
|
||||||
await SendPayloadAsync(tag, type, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload, bool 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);
|
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)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var currentContext = _httpContextAccessor?.HttpContext?.
|
var currentContext = _httpContextAccessor?.HttpContext?.
|
||||||
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
|
RequestServices.GetService(typeof(CurrentContext)) as CurrentContext;
|
||||||
if(!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
|
return currentContext?.DeviceIdentifier;
|
||||||
{
|
|
||||||
tag += $" && !deviceIdentifier:{currentContext.DeviceIdentifier}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string BuildTag(string tag, string identifier)
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(identifier))
|
||||||
|
{
|
||||||
|
tag += $" && !deviceIdentifier:{identifier}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return $"({tag})";
|
return $"({tag})";
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,42 +1,21 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Text;
|
|
||||||
using System;
|
using System;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using System.Net;
|
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
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 dynamic _decodedToken;
|
||||||
private DateTime? _nextAuthAttempt = null;
|
private DateTime? _nextAuthAttempt = null;
|
||||||
|
|
||||||
public RelayPushRegistrationService(
|
public RelayPushRegistrationService(GlobalSettings globalSettings)
|
||||||
GlobalSettings globalSettings)
|
: base(globalSettings)
|
||||||
{
|
{ }
|
||||||
_globalSettings = globalSettings;
|
|
||||||
|
|
||||||
_pushClient = new HttpClient
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(globalSettings.PushRelayBaseUri)
|
|
||||||
};
|
|
||||||
|
|
||||||
_identityClient = new HttpClient
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(globalSettings.Installation.IdentityUri)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
string identifier, DeviceType type)
|
string identifier, DeviceType type)
|
||||||
@ -56,12 +35,12 @@ namespace Bit.Core.Services
|
|||||||
UserId = userId
|
UserId = userId
|
||||||
};
|
};
|
||||||
|
|
||||||
var message = new TokenHttpRequestMessage(requestModel, _accessToken)
|
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||||
{
|
{
|
||||||
Method = HttpMethod.Post,
|
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)
|
public async Task DeleteRegistrationAsync(string deviceId)
|
||||||
@ -72,12 +51,12 @@ namespace Bit.Core.Services
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = new TokenHttpRequestMessage(_accessToken)
|
var message = new TokenHttpRequestMessage(AccessToken)
|
||||||
{
|
{
|
||||||
Method = HttpMethod.Delete,
|
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<string> deviceIds, string organizationId)
|
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
@ -94,12 +73,12 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||||
var message = new TokenHttpRequestMessage(requestModel, _accessToken)
|
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||||
{
|
{
|
||||||
Method = HttpMethod.Put,
|
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<string> deviceIds, string organizationId)
|
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
@ -116,138 +95,12 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||||
var message = new TokenHttpRequestMessage(requestModel, _accessToken)
|
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||||
{
|
{
|
||||||
Method = HttpMethod.Put,
|
Method = HttpMethod.Put,
|
||||||
RequestUri = new Uri(_pushClient.BaseAddress, "delete-organization")
|
RequestUri = new Uri(PushClient.BaseAddress, "delete-organization")
|
||||||
};
|
};
|
||||||
await _pushClient.SendAsync(message);
|
await PushClient.SendAsync(message);
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> 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<string, string>
|
|
||||||
{
|
|
||||||
{ "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<long>());
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
@ -55,5 +56,15 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,5 +289,41 @@ namespace Bit.Core.Utilities
|
|||||||
|
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,21 +71,26 @@ namespace Bit.Core.Utilities
|
|||||||
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET461
|
if(globalSettings.SelfHosted &&
|
||||||
if(globalSettings.SelfHosted)
|
CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||||
|
globalSettings.Installation?.Id != null &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
||||||
{
|
{
|
||||||
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
services.AddSingleton<IPushNotificationService, RelayPushNotificationService>();
|
||||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||||
}
|
}
|
||||||
else
|
#if NET461
|
||||||
|
else if(!globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IPushNotificationService, NotificationHubPushNotificationService>();
|
services.AddSingleton<IPushNotificationService, NotificationHubPushNotificationService>();
|
||||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||||
}
|
}
|
||||||
#else
|
#endif
|
||||||
|
else
|
||||||
|
{
|
||||||
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
||||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||||
#endif
|
}
|
||||||
|
|
||||||
if(CoreHelpers.SettingHasValue(globalSettings.Storage.ConnectionString))
|
if(CoreHelpers.SettingHasValue(globalSettings.Storage.ConnectionString))
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user