bitwarden-mobile/src/Core/Services/AzureFileUploadService.cs

198 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Bit.Core.Abstractions;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
namespace Bit.Core.Services
{
public class AzureFileUploadService : IAzureFileUploadService
{
private const long MAX_SINGLE_BLOB_UPLOAD_SIZE = 256 * 1024 * 1024; // 256 MiB
private const int MAX_BLOCKS_PER_BLOB = 50000;
private const decimal MAX_MOBILE_BLOCK_SIZE = 5 * 1024 * 1024; // 5 MB
private readonly HttpClient _httpClient = new HttpClient();
public AzureFileUploadService()
{
_httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue()
{
NoCache = true,
};
}
public async Task Upload(string uri, EncByteArray data, Func<Task<string>> renewalCallback)
{
if (data?.Buffer?.Length <= MAX_SINGLE_BLOB_UPLOAD_SIZE)
{
await AzureUploadBlob(uri, data);
}
else
{
await AzureUploadBlocks(uri, data, renewalCallback);
}
}
private async Task AzureUploadBlob(string uri, EncByteArray data)
{
using (var requestMessage = new HttpRequestMessage())
{
var uriBuilder = new UriBuilder(uri);
var paramValues = HttpUtility.ParseQueryString(uriBuilder.Query);
requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R"));
requestMessage.Headers.Add("x-ms-version", paramValues["sv"]);
requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob");
requestMessage.Content = new ByteArrayContent(data.Buffer);
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Put;
requestMessage.RequestUri = uriBuilder.Uri;
var blobResponse = await _httpClient.SendAsync(requestMessage);
if (blobResponse.StatusCode != HttpStatusCode.Created)
{
throw new Exception("Failed to create Azure blob");
}
}
}
private async Task AzureUploadBlocks(string uri, EncByteArray data, Func<Task<string>> renewalFunc)
{
_httpClient.Timeout = TimeSpan.FromHours(3);
var baseParams = HttpUtility.ParseQueryString(CoreHelpers.GetUri(uri).Query);
var blockSize = MaxBlockSize(baseParams["sv"]);
var blockIndex = 0;
var numBlocks = Math.Ceiling((decimal)data.Buffer.Length / blockSize);
var blocksStaged = new List<string>();
if (numBlocks > MAX_BLOCKS_PER_BLOB)
{
throw new Exception($"Cannot upload file, exceeds maximum size of {blockSize * MAX_BLOCKS_PER_BLOB}");
}
while (blockIndex < numBlocks)
{
uri = await RenewUriIfNecessary(uri, renewalFunc);
var blockUriBuilder = new UriBuilder(uri);
var blockId = EncodeBlockId(blockIndex);
var blockParams = HttpUtility.ParseQueryString(blockUriBuilder.Query);
blockParams.Add("comp", "block");
blockParams.Add("blockid", blockId);
blockUriBuilder.Query = blockParams.ToString();
using (var requestMessage = new HttpRequestMessage())
{
requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R"));
requestMessage.Headers.Add("x-ms-version", baseParams["sv"]);
requestMessage.Headers.Add("x-ms-blob-type", "BlockBlob");
requestMessage.Content = new ByteArrayContent(data.Buffer.Skip(blockIndex * blockSize).Take(blockSize).ToArray());
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Put;
requestMessage.RequestUri = blockUriBuilder.Uri;
var blockResponse = await _httpClient.SendAsync(requestMessage);
if (blockResponse.StatusCode != HttpStatusCode.Created)
{
throw new Exception("Failed to create Azure block");
}
}
blocksStaged.Add(blockId);
blockIndex++;
}
using (var requestMessage = new HttpRequestMessage())
{
uri = await RenewUriIfNecessary(uri, renewalFunc);
var blockListXml = GenerateBlockListXml(blocksStaged);
var blockListUriBuilder = new UriBuilder(uri);
var blockListParams = HttpUtility.ParseQueryString(blockListUriBuilder.Query);
blockListParams.Add("comp", "blocklist");
blockListUriBuilder.Query = blockListParams.ToString();
requestMessage.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R"));
requestMessage.Headers.Add("x-ms-version", baseParams["sv"]);
requestMessage.Content = new StringContent(blockListXml);
requestMessage.Version = new Version(1, 0);
requestMessage.Method = HttpMethod.Put;
requestMessage.RequestUri = blockListUriBuilder.Uri;
var blockListResponse = await _httpClient.SendAsync(requestMessage);
if (blockListResponse.StatusCode != HttpStatusCode.Created)
{
throw new Exception("Failed to PUT Azure block list");
}
}
}
private async Task<string> RenewUriIfNecessary(string uri, Func<Task<string>> renewalFunc)
{
var uriParams = HttpUtility.ParseQueryString(CoreHelpers.GetUri(uri).Query);
if (DateTime.TryParse(uriParams.Get("se") ?? "", out DateTime expiry) && expiry < DateTime.UtcNow.AddSeconds(1))
{
return await renewalFunc();
}
return uri;
}
private string GenerateBlockListXml(List<string> blocksStaged)
{
var xml = new StringBuilder("<?xml version=\"1.0\" encoding=\"utf-8\"?><BlockList>");
foreach(var blockId in blocksStaged)
{
xml.Append($"<Latest>{blockId}</Latest>");
}
xml.Append("</BlockList>");
return xml.ToString();
}
private string EncodeBlockId(int index)
{
// Encoded blockId max size is 64, so pre-encoding max size is 48
var paddedString = index.ToString("D48");
return Convert.ToBase64String(Encoding.UTF8.GetBytes(paddedString));
}
private int MaxBlockSize(string version)
{
long maxSize = 4194304L; // 4 MiB
if (CompareAzureVersions(version, "2019-12-12") >= 0)
{
maxSize = 4194304000L; // 4000 MiB
}
else if (CompareAzureVersions(version, "2016-05-31") >= 0)
{
maxSize = 104857600L; // 100 MiB
}
return maxSize > MAX_MOBILE_BLOCK_SIZE ? (int)MAX_MOBILE_BLOCK_SIZE : (int) maxSize;
}
private int CompareAzureVersions(string a, string b)
{
var v1Parts = a.Split('-').Select(p => int.Parse(p));
var v2Parts = b.Split('-').Select(p => int.Parse(p));
return a[0] != b[0] ? a[0] - b[0] :
a[1] != b[1] ? a[1] - b[1] :
a[2] != b[2] ? a[2] - b[2] :
0;
}
}
}