using System.Text.Json; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; namespace Bit.Core.Tools.SendFeatures.Commands; public class NonAnonymousSendCommand : INonAnonymousSendCommand { private readonly ISendRepository _sendRepository; private readonly ISendFileStorageService _sendFileStorageService; private readonly IPushNotificationService _pushNotificationService; private readonly ISendValidationService _sendValidationService; private readonly ISendCoreHelperService _sendCoreHelperService; private readonly ILogger _logger; public NonAnonymousSendCommand(ISendRepository sendRepository, ISendFileStorageService sendFileStorageService, IPushNotificationService pushNotificationService, ISendAuthorizationService sendAuthorizationService, ISendValidationService sendValidationService, ISendCoreHelperService sendCoreHelperService, ILogger logger) { _sendRepository = sendRepository; _sendFileStorageService = sendFileStorageService; _pushNotificationService = pushNotificationService; _sendValidationService = sendValidationService; _sendCoreHelperService = sendCoreHelperService; _logger = logger; } public async Task SaveSendAsync(Send send) { // Make sure user can save Sends await _sendValidationService.ValidateUserCanSaveAsync(send.UserId, send); if (send.Id == default(Guid)) { await _sendRepository.CreateAsync(send); await _pushNotificationService.PushSyncSendCreateAsync(send); } else { send.RevisionDate = DateTime.UtcNow; await _sendRepository.UpsertAsync(send); await _pushNotificationService.PushSyncSendUpdateAsync(send); } } public async Task SaveFileSendAsync(Send send, SendFileData data, long fileLength) { if (send.Type != SendType.File) { throw new BadRequestException("Send is not of type \"file\"."); } if (fileLength < 1) { throw new BadRequestException("No file data."); } if (fileLength > SendFileSettingHelper.MAX_FILE_SIZE) { throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}."); } var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send); if (storageBytesRemaining < fileLength) { throw new BadRequestException("Not enough storage available."); } var fileId = _sendCoreHelperService.SecureRandomString(32, useUpperCase: false, useSpecial: false); try { data.Id = fileId; data.Size = fileLength; data.Validated = false; send.Data = JsonSerializer.Serialize(data, JsonHelpers.IgnoreWritingNull); await SaveSendAsync(send); return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId); } catch { _logger.LogWarning( "Deleted file from {SendId} because an error occurred when creating the upload URL.", send.Id ); // Clean up since this is not transactional await _sendFileStorageService.DeleteFileAsync(send, fileId); throw; } } public async Task UploadFileToExistingSendAsync(Stream stream, Send send) { if (stream.Position > 0) { stream.Position = 0; } if (send?.Data == null) { throw new BadRequestException("Send does not have file data"); } if (send.Type != SendType.File) { throw new BadRequestException("Not a File Type Send."); } var data = JsonSerializer.Deserialize(send.Data); if (data.Validated) { throw new BadRequestException("File has already been uploaded."); } await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id); if (!await ConfirmFileSize(send)) { throw new BadRequestException("File received does not match expected file length."); } } public async Task DeleteSendAsync(Send send) { await _sendRepository.DeleteAsync(send); if (send.Type == Enums.SendType.File) { var data = JsonSerializer.Deserialize(send.Data); await _sendFileStorageService.DeleteFileAsync(send, data.Id); } await _pushNotificationService.PushSyncSendDeleteAsync(send); } public async Task ConfirmFileSize(Send send) { var fileData = JsonSerializer.Deserialize(send.Data); var minimum = fileData.Size - SendFileSettingHelper.FILE_SIZE_LEEWAY; var maximum = Math.Min( fileData.Size + SendFileSettingHelper.FILE_SIZE_LEEWAY, SendFileSettingHelper.MAX_FILE_SIZE ); var (valid, size) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, minimum, maximum); // protect file service from upload hijacking by deleting invalid sends if (!valid) { _logger.LogWarning( "Deleted {SendId} because its reported size {Size} was outside the expected range ({Minimum} - {Maximum}).", send.Id, size, minimum, maximum ); await DeleteSendAsync(send); return false; } // replace expected size with validated size fileData.Size = size; fileData.Validated = true; send.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull); await SaveSendAsync(send); return valid; } }