1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 00:22:50 -05:00

attachment apis and azure storage service

This commit is contained in:
Kyle Spearrin
2017-06-15 15:34:12 -04:00
parent 94be5bc1dd
commit 06ca566be1
9 changed files with 232 additions and 0 deletions

View File

@ -8,6 +8,7 @@ using Bit.Core.Models.Api;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core;
using Bit.Api.Utilities;
namespace Bit.Api.Controllers
{
@ -19,6 +20,7 @@ namespace Bit.Api.Controllers
private readonly ICollectionCipherRepository _collectionCipherRepository;
private readonly ICipherService _cipherService;
private readonly IUserService _userService;
private readonly IAttachmentStorageService _attachmentStorageService;
private readonly CurrentContext _currentContext;
public CiphersController(
@ -26,12 +28,14 @@ namespace Bit.Api.Controllers
ICollectionCipherRepository collectionCipherRepository,
ICipherService cipherService,
IUserService userService,
IAttachmentStorageService attachmentStorageService,
CurrentContext currentContext)
{
_cipherRepository = cipherRepository;
_collectionCipherRepository = collectionCipherRepository;
_cipherService = cipherService;
_userService = userService;
_attachmentStorageService = attachmentStorageService;
_currentContext = currentContext;
}
@ -214,5 +218,57 @@ namespace Bit.Api.Controllers
await _cipherService.MoveManyAsync(model.Ids.Select(i => new Guid(i)),
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
}
[HttpPost("attachment")]
[DisableFormValueModelBinding]
public async Task Post(string id)
{
// throw for now
throw new NotImplementedException();
if(!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId);
if(cipher == null)
{
throw new NotFoundException();
}
await Request.GetFilesAsync(async (stream, fileName) =>
{
var attachmentId = Guid.NewGuid();
// TODO: store attachmentId + fileName reference in database
var storedFilename = $"{idGuid}_{attachmentId}";
await _attachmentStorageService.UploadAttachmentAsync(stream, storedFilename);
});
}
[HttpDelete("{id}/attachment/{attachmentId}")]
[HttpPost("{id}/attachment/{attachmentId}/delete")]
public async Task Delete(string id, string attachmentId)
{
// throw for now
throw new NotImplementedException();
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId);
if(cipher == null)
{
throw new NotFoundException();
}
var attachmentIdGuid = new Guid(attachmentId);
// TODO: check and remove attachmentId from cipher in database
var storedFilename = $"{idGuid}_{attachmentId}";
await _attachmentStorageService.DeleteAttachmentAsync(storedFilename);
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq;
namespace Bit.Api.Utilities
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValue = context.ValueProviderFactories.OfType<FormValueProviderFactory>().FirstOrDefault();
if(formValue != null)
{
context.ValueProviderFactories.Remove(formValue);
}
var jqFormValue = context.ValueProviderFactories.OfType<JQueryFormValueProviderFactory>().FirstOrDefault();
if(jqFormValue != null)
{
context.ValueProviderFactories.Remove(jqFormValue);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
}

View File

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Bit.Api.Utilities
{
public static class MultipartFormDataHelper
{
private static readonly FormOptions _defaultFormOptions = new FormOptions();
public static async Task GetFilesAsync(this HttpRequest request, Func<Stream, string, Task> callback)
{
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body);
var section = await reader.ReadNextSectionAsync();
while(section != null)
{
ContentDispositionHeaderValue content;
if(ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out content) &&
HasFileContentDisposition(content))
{
await callback(section.Body, HeaderUtilities.RemoveQuotes(content.FileName));
}
section = await reader.ReadNextSectionAsync();
}
}
private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
if(string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if(boundary.Length > lengthLimit)
{
throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
private static bool HasFileContentDisposition(ContentDispositionHeaderValue content)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return content != null && content.DispositionType.Equals("form-data") &&
(!string.IsNullOrEmpty(content.FileName) || !string.IsNullOrEmpty(content.FileNameStar));
}
}
}

View File

@ -27,6 +27,10 @@
"storage": {
"connectionString": "SECRET"
},
"attachment": {
"connectionString": "SECRET",
"baseUrl": "http://localhost:4000/"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"