mirror of
https://github.com/bitwarden/server.git
synced 2025-02-01 23:31:41 +01:00
Direct upload to Azure/Local (#1188)
* Direct upload to azure To validate file sizes in the event of a rogue client, Azure event webhooks will be hooked up to AzureValidateFile. Sends outside of a grace size will be deleted as non-compliant. TODO: LocalSendFileStorageService direct upload method/endpoint. * Quick respond to no-body event calls These shouldn't happen, but might if some errant get requests occur * Event Grid only POSTS to webhook * Enable local storage direct file upload * Increase file size difference leeway * Upload through service * Fix LocalFileSendStorage It turns out that multipartHttpStreams do not have a length until read. this causes all long files to be "invalid". We need to write the entire stream, then validate length, just like Azure. the difference is, We can return an exception to local storage admonishing the client for lying * Update src/Api/Utilities/ApiHelpers.cs Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Do not delete directory if it has files * Allow large uploads for self hosted instances * Fix formatting * Re-verfiy access and increment access count on download of Send File * Update src/Core/Services/Implementations/SendService.cs Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Add back in original Send upload * Update size and mark as validated upon Send file validation * Log azure file validation errors * Lint fix Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
This commit is contained in:
parent
fd42b227b3
commit
989d4df599
@ -26,6 +26,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.6" />
|
||||
<PackageReference Include="NewRelic.Agent" Version="8.30.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
|
||||
<PackageReference Include="Microsoft.Azure.EventGrid" Version="3.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -7,11 +7,17 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Enums;
|
||||
using Microsoft.Azure.EventGrid.Models;
|
||||
using Bit.Api.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Models.Table;
|
||||
using Newtonsoft.Json;
|
||||
using Bit.Core.Models.Data;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Api.Controllers
|
||||
{
|
||||
@ -23,6 +29,7 @@ namespace Bit.Api.Controllers
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ISendFileStorageService _sendFileStorageService;
|
||||
private readonly ILogger<SendsController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public SendsController(
|
||||
@ -30,12 +37,14 @@ namespace Bit.Api.Controllers
|
||||
IUserService userService,
|
||||
ISendService sendService,
|
||||
ISendFileStorageService sendFileStorageService,
|
||||
ILogger<SendsController> logger,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_userService = userService;
|
||||
_sendService = sendService;
|
||||
_sendFileStorageService = sendFileStorageService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
@ -160,12 +169,113 @@ namespace Bit.Api.Controllers
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService);
|
||||
send = madeSend;
|
||||
await _sendService.CreateSendAsync(send, madeData, stream, model.FileLength.GetValueOrDefault(0));
|
||||
await _sendService.SaveFileSendAsync(send, madeData, model.FileLength.GetValueOrDefault(0));
|
||||
await _sendService.UploadFileToExistingSendAsync(stream, send);
|
||||
});
|
||||
|
||||
return new SendResponseModel(send, _globalSettings);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("file/v2")]
|
||||
public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)
|
||||
{
|
||||
if (model.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Invalid content.");
|
||||
}
|
||||
|
||||
if (!model.FileLength.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Invalid content. File size hint is required.");
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
|
||||
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
|
||||
return new SendFileUploadDataResponseModel
|
||||
{
|
||||
Url = uploadUrl,
|
||||
FileUploadType = _sendFileStorageService.FileUploadType,
|
||||
SendResponse = new SendResponseModel(send, _globalSettings)
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("{id}/file/{fileId}")]
|
||||
public async Task<SendFileUploadDataResponseModel> RenewFileUpload(string id, string fileId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var sendId = new Guid(id);
|
||||
var send = await _sendRepository.GetByIdAsync(sendId);
|
||||
var fileData = JsonConvert.DeserializeObject<SendFileData>(send?.Data);
|
||||
|
||||
if (send == null || send.Type != SendType.File || (send.UserId.HasValue && send.UserId.Value != userId) ||
|
||||
!send.UserId.HasValue || fileData.Id != fileId || fileData.Validated)
|
||||
{
|
||||
// Not found if Send isn't found, user doesn't have access, request is faulty,
|
||||
// or we've already validated the file. This last is to emulate create-only blob permissions for Azure
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new SendFileUploadDataResponseModel
|
||||
{
|
||||
Url = await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId),
|
||||
FileUploadType = _sendFileStorageService.FileUploadType,
|
||||
SendResponse = new SendResponseModel(send, _globalSettings),
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("{id}/file/{fileId}")]
|
||||
[DisableFormValueModelBinding]
|
||||
public async Task PostFileForExistingSend(string id, string fileId)
|
||||
{
|
||||
if (!Request?.ContentType.Contains("multipart/") ?? true)
|
||||
{
|
||||
throw new BadRequestException("Invalid content.");
|
||||
}
|
||||
|
||||
if (Request.ContentLength > 105906176 && !_globalSettings.SelfHosted) // 101 MB, give em' 1 extra MB for cushion
|
||||
{
|
||||
throw new BadRequestException("Max file size for direct upload is 100 MB.");
|
||||
}
|
||||
|
||||
var send = await _sendRepository.GetByIdAsync(new Guid(id));
|
||||
await Request.GetSendFileAsync(async (stream) =>
|
||||
{
|
||||
await _sendService.UploadFileToExistingSendAsync(stream, send);
|
||||
});
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("file/validate/azure")]
|
||||
public async Task<OkObjectResult> AzureValidateFile()
|
||||
{
|
||||
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
|
||||
{
|
||||
{
|
||||
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
|
||||
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
|
||||
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
|
||||
if (send == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await _sendService.ValidateSendFile(send);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonConvert.SerializeObject(eventGridEvent)}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<SendResponseModel> Put(string id, [FromBody] SendRequestModel model)
|
||||
{
|
||||
|
@ -1,5 +1,10 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Azure.EventGrid;
|
||||
using Microsoft.Azure.EventGrid.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -29,5 +34,47 @@ namespace Bit.Api.Utilities
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Azure event subscription and calls the appropriate event handler. Responds HttpOk.
|
||||
/// </summary>
|
||||
/// <param name="request">HttpRequest received from Azure</param>
|
||||
/// <param name="eventTypeHandlers">Dictionary of eventType strings and their associated handlers.</param>
|
||||
/// <returns>OkObjectResult</returns>
|
||||
/// <remarks>Reference https://docs.microsoft.com/en-us/azure/event-grid/receive-events</remarks>
|
||||
public async static Task<OkObjectResult> HandleAzureEvents(HttpRequest request,
|
||||
Dictionary<string, Func<EventGridEvent, Task>> eventTypeHandlers)
|
||||
{
|
||||
var response = string.Empty;
|
||||
var requestContent = await new StreamReader(request.Body).ReadToEndAsync();
|
||||
if (string.IsNullOrWhiteSpace(requestContent))
|
||||
{
|
||||
return new OkObjectResult(response);
|
||||
}
|
||||
|
||||
var eventGridSubscriber = new EventGridSubscriber();
|
||||
var eventGridEvents = eventGridSubscriber.DeserializeEventGridEvents(requestContent);
|
||||
|
||||
foreach (var eventGridEvent in eventGridEvents)
|
||||
{
|
||||
if (eventGridEvent.Data is SubscriptionValidationEventData eventData)
|
||||
{
|
||||
// Might want to enable additional validation: subject, topic etc.
|
||||
|
||||
var responseData = new SubscriptionValidationResponse()
|
||||
{
|
||||
ValidationResponse = eventData.ValidationCode
|
||||
};
|
||||
|
||||
return new OkObjectResult(responseData);
|
||||
}
|
||||
else if (eventTypeHandlers.ContainsKey(eventGridEvent.EventType))
|
||||
{
|
||||
await eventTypeHandlers[eventGridEvent.EventType](eventGridEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return new OkObjectResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,27 @@ namespace Bit.Api.Utilities
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, Task> callback)
|
||||
{
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
var reader = new MultipartReader(boundary, request.Body);
|
||||
|
||||
var dataSection = await reader.ReadNextSectionAsync();
|
||||
if (dataSection != null)
|
||||
{
|
||||
if (ContentDispositionHeaderValue.TryParse(dataSection.ContentDisposition, out var dataContent)
|
||||
&& HasFileContentDisposition(dataContent))
|
||||
{
|
||||
using (dataSection.Body)
|
||||
{
|
||||
await callback(dataSection.Body);
|
||||
}
|
||||
}
|
||||
dataSection = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
|
||||
{
|
||||
|
8
src/Core/Enums/FileUploadType.cs
Normal file
8
src/Core/Enums/FileUploadType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum FileUploadType
|
||||
{
|
||||
Direct = 0,
|
||||
Azure = 1,
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ namespace Bit.Core.Models.Api
|
||||
public class SendRequestModel
|
||||
{
|
||||
public SendType Type { get; set; }
|
||||
public long? FileLength { get; set; }
|
||||
public long? FileLength { get; set; } = null;
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public string Name { get; set; }
|
||||
|
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api.Response
|
||||
{
|
||||
public class SendFileUploadDataResponseModel : ResponseModel
|
||||
{
|
||||
public SendFileUploadDataResponseModel() : base("send-fileUpload") { }
|
||||
|
||||
public string Url { get; set; }
|
||||
public FileUploadType FileUploadType { get; set; }
|
||||
public SendResponseModel SendResponse { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -33,5 +33,6 @@ namespace Bit.Core.Models.Data
|
||||
|
||||
public string Id { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public bool Validated { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,11 @@ namespace Bit.Core.Services
|
||||
{
|
||||
Task DeleteSendAsync(Send send);
|
||||
Task SaveSendAsync(Send send);
|
||||
Task CreateSendAsync(Send send, SendFileData data, Stream stream, long requestLength);
|
||||
Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength);
|
||||
Task UploadFileToExistingSendAsync(Stream stream, Send send);
|
||||
Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password);
|
||||
string HashPassword(string password);
|
||||
Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password);
|
||||
Task<bool> ValidateSendFile(Send send);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Table;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
@ -7,10 +8,13 @@ namespace Bit.Core.Services
|
||||
{
|
||||
public interface ISendFileStorageService
|
||||
{
|
||||
FileUploadType FileUploadType { get; }
|
||||
Task UploadNewFileAsync(Stream stream, Send send, string fileId);
|
||||
Task DeleteFileAsync(Send send, string fileId);
|
||||
Task DeleteFilesForOrganizationAsync(Guid organizationId);
|
||||
Task DeleteFilesForUserAsync(Guid userId);
|
||||
Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId);
|
||||
Task<string> GetSendFileUploadUrlAsync(Send send, string fileId);
|
||||
Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.IO;
|
||||
using System;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -15,6 +16,8 @@ namespace Bit.Core.Services
|
||||
private readonly CloudBlobClient _blobClient;
|
||||
private CloudBlobContainer _sendFilesContainer;
|
||||
|
||||
public FileUploadType FileUploadType => FileUploadType.Azure;
|
||||
|
||||
public static string SendIdFromBlobName(string blobName) => blobName.Split('/')[0];
|
||||
public static string BlobName(Send send, string fileId) => $"{send.Id}/{fileId}";
|
||||
|
||||
@ -71,6 +74,54 @@ namespace Bit.Core.Services
|
||||
return blob.Uri + blob.GetSharedAccessSignature(accessPolicy);
|
||||
}
|
||||
|
||||
public async Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)
|
||||
{
|
||||
await InitAsync();
|
||||
var blob = _sendFilesContainer.GetBlockBlobReference(BlobName(send, fileId));
|
||||
|
||||
var accessPolicy = new SharedAccessBlobPolicy()
|
||||
{
|
||||
SharedAccessExpiryTime = DateTime.UtcNow.Add(_downloadLinkLiveTime),
|
||||
Permissions = SharedAccessBlobPermissions.Create | SharedAccessBlobPermissions.Write,
|
||||
};
|
||||
|
||||
return blob.Uri + blob.GetSharedAccessSignature(accessPolicy);
|
||||
}
|
||||
|
||||
public async Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
||||
{
|
||||
await InitAsync();
|
||||
|
||||
var blob = _sendFilesContainer.GetBlockBlobReference(BlobName(send, fileId));
|
||||
|
||||
if (!blob.Exists())
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
blob.FetchAttributes();
|
||||
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
blob.Metadata["userId"] = send.UserId.Value.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
blob.Metadata["organizationId"] = send.OrganizationId.Value.ToString();
|
||||
}
|
||||
blob.Properties.ContentDisposition = $"attachment; filename=\"{fileId}\"";
|
||||
blob.SetMetadata();
|
||||
blob.SetProperties();
|
||||
|
||||
var length = blob.Properties.Length;
|
||||
if (length < expectedFileSize - leeway || length > expectedFileSize + leeway)
|
||||
{
|
||||
return (false, length);
|
||||
}
|
||||
|
||||
return (true, length);
|
||||
}
|
||||
|
||||
private async Task InitAsync()
|
||||
{
|
||||
if (_sendFilesContainer == null)
|
||||
|
@ -4,6 +4,7 @@ using System;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Settings;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -14,6 +15,7 @@ namespace Bit.Core.Services
|
||||
|
||||
private string RelativeFilePath(Send send, string fileID) => $"{send.Id}/{fileID}";
|
||||
private string FilePath(Send send, string fileID) => $"{_baseDirPath}/{RelativeFilePath(send, fileID)}";
|
||||
public FileUploadType FileUploadType => FileUploadType.Direct;
|
||||
|
||||
public LocalSendStorageService(
|
||||
GlobalSettings globalSettings)
|
||||
@ -83,5 +85,26 @@ namespace Bit.Core.Services
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)
|
||||
=> Task.FromResult($"/sends/{send.Id}/file/{fileId}");
|
||||
|
||||
public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
||||
{
|
||||
long? length = null;
|
||||
var path = FilePath(send, fileId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return Task.FromResult((false, length));
|
||||
}
|
||||
|
||||
length = new FileInfo(path).Length;
|
||||
if (expectedFileSize < length - leeway || expectedFileSize > length + leeway)
|
||||
{
|
||||
return Task.FromResult((false, length));
|
||||
}
|
||||
|
||||
return Task.FromResult((true, length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ namespace Bit.Core.Services
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
|
||||
|
||||
public SendService(
|
||||
ISendRepository sendRepository,
|
||||
@ -74,51 +75,21 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateSendAsync(Send send, SendFileData data, Stream stream, long requestLength)
|
||||
public async Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength)
|
||||
{
|
||||
if (send.Type != SendType.File)
|
||||
{
|
||||
throw new BadRequestException("Send is not of type \"file\".");
|
||||
}
|
||||
|
||||
if (requestLength < 1)
|
||||
if (fileLength < 1)
|
||||
{
|
||||
throw new BadRequestException("No file data.");
|
||||
}
|
||||
|
||||
var storageBytesRemaining = 0L;
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(send.UserId.Value);
|
||||
if (!(await _userService.CanAccessPremium(user)))
|
||||
{
|
||||
throw new BadRequestException("You must have premium status to use file sends.");
|
||||
}
|
||||
var storageBytesRemaining = await StorageRemainingForSendAsync(send);
|
||||
|
||||
if (user.Premium)
|
||||
{
|
||||
storageBytesRemaining = user.StorageBytesRemaining();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Users that get access to file storage/premium from their organization get the default
|
||||
// 1 GB max storage.
|
||||
storageBytesRemaining = user.StorageBytesRemaining(
|
||||
_globalSettings.SelfHosted ? (short)10240 : (short)1);
|
||||
}
|
||||
}
|
||||
else if (send.OrganizationId.HasValue)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value);
|
||||
if (!org.MaxStorageGb.HasValue)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use file sends.");
|
||||
}
|
||||
|
||||
storageBytesRemaining = org.StorageBytesRemaining();
|
||||
}
|
||||
|
||||
if (storageBytesRemaining < requestLength)
|
||||
if (storageBytesRemaining < fileLength)
|
||||
{
|
||||
throw new BadRequestException("Not enough storage available.");
|
||||
}
|
||||
@ -128,24 +99,12 @@ namespace Bit.Core.Services
|
||||
try
|
||||
{
|
||||
data.Id = fileId;
|
||||
data.Size = fileLength;
|
||||
data.Validated = false;
|
||||
send.Data = JsonConvert.SerializeObject(data,
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
||||
await SaveSendAsync(send);
|
||||
await _sendFileStorageService.UploadNewFileAsync(stream, send, fileId);
|
||||
// Need to save length of stream since that isn't available until it is read
|
||||
if (stream.Length <= requestLength)
|
||||
{
|
||||
data.Size = stream.Length;
|
||||
send.Data = JsonConvert.SerializeObject(data,
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
||||
await SaveSendAsync(send);
|
||||
}
|
||||
else
|
||||
{
|
||||
await DeleteSendAsync(send);
|
||||
throw new BadRequestException("Content-Length header is smaller than file received.");
|
||||
}
|
||||
|
||||
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -155,6 +114,53 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFileToExistingSendAsync(Stream stream, Send send)
|
||||
{
|
||||
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 = JsonConvert.DeserializeObject<SendFileData>(send.Data);
|
||||
|
||||
await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id);
|
||||
|
||||
if (!await ValidateSendFile(send))
|
||||
{
|
||||
throw new BadRequestException("File received does not match expected file length.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateSendFile(Send send)
|
||||
{
|
||||
var fileData = JsonConvert.DeserializeObject<SendFileData>(send.Data);
|
||||
|
||||
var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, _fileSizeLeeway);
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
// File reported differs in size from that promised. Must be a rogue client. Delete Send
|
||||
await DeleteSendAsync(send);
|
||||
}
|
||||
|
||||
// Update Send data if necessary
|
||||
if (realSize != fileData.Size)
|
||||
{
|
||||
fileData.Size = realSize.Value;
|
||||
}
|
||||
fileData.Validated = true;
|
||||
send.Data = JsonConvert.SerializeObject(fileData,
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
||||
await SaveSendAsync(send);
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public async Task DeleteSendAsync(Send send)
|
||||
{
|
||||
await _sendRepository.DeleteAsync(send);
|
||||
@ -281,5 +287,42 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<long> StorageRemainingForSendAsync(Send send)
|
||||
{
|
||||
var storageBytesRemaining = 0L;
|
||||
if (send.UserId.HasValue)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(send.UserId.Value);
|
||||
if (!await _userService.CanAccessPremium(user))
|
||||
{
|
||||
throw new BadRequestException("You must have premium status to use file sends.");
|
||||
}
|
||||
|
||||
if (user.Premium)
|
||||
{
|
||||
storageBytesRemaining = user.StorageBytesRemaining();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Users that get access to file storage/premium from their organization get the default
|
||||
// 1 GB max storage.
|
||||
storageBytesRemaining = user.StorageBytesRemaining(
|
||||
_globalSettings.SelfHosted ? (short)10240 : (short)1);
|
||||
}
|
||||
}
|
||||
else if (send.OrganizationId.HasValue)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value);
|
||||
if (!org.MaxStorageGb.HasValue)
|
||||
{
|
||||
throw new BadRequestException("This organization cannot use file sends.");
|
||||
}
|
||||
|
||||
storageBytesRemaining = org.StorageBytesRemaining();
|
||||
}
|
||||
|
||||
return storageBytesRemaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class NoopSendFileStorageService : ISendFileStorageService
|
||||
{
|
||||
public FileUploadType FileUploadType => FileUploadType.Direct;
|
||||
|
||||
public Task UploadNewFileAsync(Stream stream, Send send, string attachmentId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
@ -31,5 +34,15 @@ namespace Bit.Core.Services
|
||||
{
|
||||
return Task.FromResult((string)null);
|
||||
}
|
||||
|
||||
public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)
|
||||
{
|
||||
return Task.FromResult((string)null);
|
||||
}
|
||||
|
||||
public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
||||
{
|
||||
return Task.FromResult((false, default(long?)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user