mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
Turn on file scoped namespaces (#2225)
This commit is contained in:
@ -1,13 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public class ApiExplorerGroupConvention : IControllerModelConvention
|
||||
{
|
||||
public class ApiExplorerGroupConvention : IControllerModelConvention
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
var controllerNamespace = controller.ControllerType.Namespace;
|
||||
controller.ApiExplorer.GroupName = controllerNamespace.Contains(".Public.") ? "public" : "internal";
|
||||
}
|
||||
var controllerNamespace = controller.ControllerType.Namespace;
|
||||
controller.ApiExplorer.GroupName = controllerNamespace.Contains(".Public.") ? "public" : "internal";
|
||||
}
|
||||
}
|
||||
|
@ -4,70 +4,69 @@ using Azure.Messaging.EventGrid.SystemEvents;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public static class ApiHelpers
|
||||
{
|
||||
public static class ApiHelpers
|
||||
public static string EventGridKey { get; set; }
|
||||
public async static Task<T> ReadJsonFileFromBody<T>(HttpContext httpContext, IFormFile file, long maxSize = 51200)
|
||||
{
|
||||
public static string EventGridKey { get; set; }
|
||||
public async static Task<T> ReadJsonFileFromBody<T>(HttpContext httpContext, IFormFile file, long maxSize = 51200)
|
||||
T obj = default(T);
|
||||
if (file != null && httpContext.Request.ContentLength.HasValue && httpContext.Request.ContentLength.Value <= maxSize)
|
||||
{
|
||||
T obj = default(T);
|
||||
if (file != null && httpContext.Request.ContentLength.HasValue && httpContext.Request.ContentLength.Value <= maxSize)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = file.OpenReadStream();
|
||||
obj = await JsonSerializer.DeserializeAsync<T>(stream, JsonHelpers.IgnoreCase);
|
||||
}
|
||||
catch { }
|
||||
using var stream = file.OpenReadStream();
|
||||
obj = await JsonSerializer.DeserializeAsync<T>(stream, JsonHelpers.IgnoreCase);
|
||||
}
|
||||
|
||||
return obj;
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <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<ObjectResult> HandleAzureEvents(HttpRequest request,
|
||||
Dictionary<string, Func<EventGridEvent, Task>> eventTypeHandlers)
|
||||
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<ObjectResult> HandleAzureEvents(HttpRequest request,
|
||||
Dictionary<string, Func<EventGridEvent, Task>> eventTypeHandlers)
|
||||
{
|
||||
var queryKey = request.Query["key"];
|
||||
|
||||
if (!CoreHelpers.FixedTimeEquals(queryKey, EventGridKey))
|
||||
{
|
||||
var queryKey = request.Query["key"];
|
||||
return new UnauthorizedObjectResult("Authentication failed. Please use a valid key.");
|
||||
}
|
||||
|
||||
if (!CoreHelpers.FixedTimeEquals(queryKey, EventGridKey))
|
||||
var response = string.Empty;
|
||||
var requestData = await BinaryData.FromStreamAsync(request.Body);
|
||||
var eventGridEvents = EventGridEvent.ParseMany(requestData);
|
||||
foreach (var eventGridEvent in eventGridEvents)
|
||||
{
|
||||
if (eventGridEvent.TryGetSystemEventData(out object systemEvent))
|
||||
{
|
||||
return new UnauthorizedObjectResult("Authentication failed. Please use a valid key.");
|
||||
}
|
||||
|
||||
var response = string.Empty;
|
||||
var requestData = await BinaryData.FromStreamAsync(request.Body);
|
||||
var eventGridEvents = EventGridEvent.ParseMany(requestData);
|
||||
foreach (var eventGridEvent in eventGridEvents)
|
||||
{
|
||||
if (eventGridEvent.TryGetSystemEventData(out object systemEvent))
|
||||
if (systemEvent is SubscriptionValidationEventData eventData)
|
||||
{
|
||||
if (systemEvent is SubscriptionValidationEventData eventData)
|
||||
// Might want to enable additional validation: subject, topic etc.
|
||||
var responseData = new SubscriptionValidationResponse()
|
||||
{
|
||||
// Might want to enable additional validation: subject, topic etc.
|
||||
var responseData = new SubscriptionValidationResponse()
|
||||
{
|
||||
ValidationResponse = eventData.ValidationCode
|
||||
};
|
||||
ValidationResponse = eventData.ValidationCode
|
||||
};
|
||||
|
||||
return new OkObjectResult(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
if (eventTypeHandlers.ContainsKey(eventGridEvent.EventType))
|
||||
{
|
||||
await eventTypeHandlers[eventGridEvent.EventType](eventGridEvent);
|
||||
return new OkObjectResult(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
return new OkObjectResult(response);
|
||||
if (eventTypeHandlers.ContainsKey(eventGridEvent.EventType))
|
||||
{
|
||||
await eventTypeHandlers[eventGridEvent.EventType](eventGridEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return new OkObjectResult(response);
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
|
||||
{
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
var factories = context.ValueProviderFactories;
|
||||
factories.RemoveType<FormValueProviderFactory>();
|
||||
factories.RemoveType<FormFileValueProviderFactory>();
|
||||
factories.RemoveType<JQueryFormValueProviderFactory>();
|
||||
}
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||
{
|
||||
}
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
|
||||
{
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
var factories = context.ValueProviderFactories;
|
||||
factories.RemoveType<FormValueProviderFactory>();
|
||||
factories.RemoveType<FormFileValueProviderFactory>();
|
||||
factories.RemoveType<JQueryFormValueProviderFactory>();
|
||||
}
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -6,117 +6,116 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using Stripe;
|
||||
using InternalApi = Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
{
|
||||
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
private readonly bool _publicApi;
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public ExceptionHandlerFilterAttribute(bool publicApi)
|
||||
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
private readonly bool _publicApi;
|
||||
|
||||
public ExceptionHandlerFilterAttribute(bool publicApi)
|
||||
{
|
||||
_publicApi = publicApi;
|
||||
}
|
||||
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
var errorMessage = "An error has occurred.";
|
||||
|
||||
var exception = context.Exception;
|
||||
if (exception == null)
|
||||
{
|
||||
_publicApi = publicApi;
|
||||
// Should never happen.
|
||||
return;
|
||||
}
|
||||
|
||||
public override void OnException(ExceptionContext context)
|
||||
ErrorResponseModel publicErrorModel = null;
|
||||
InternalApi.ErrorResponseModel internalErrorModel = null;
|
||||
if (exception is BadRequestException badRequestException)
|
||||
{
|
||||
var errorMessage = "An error has occurred.";
|
||||
|
||||
var exception = context.Exception;
|
||||
if (exception == null)
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
if (badRequestException.ModelState != null)
|
||||
{
|
||||
// Should never happen.
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorResponseModel publicErrorModel = null;
|
||||
InternalApi.ErrorResponseModel internalErrorModel = null;
|
||||
if (exception is BadRequestException badRequestException)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
if (badRequestException.ModelState != null)
|
||||
{
|
||||
if (_publicApi)
|
||||
{
|
||||
publicErrorModel = new ErrorResponseModel(badRequestException.ModelState);
|
||||
}
|
||||
else
|
||||
{
|
||||
internalErrorModel = new InternalApi.ErrorResponseModel(badRequestException.ModelState);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = badRequestException.Message;
|
||||
}
|
||||
}
|
||||
else if (exception is StripeException stripeException && stripeException?.StripeError?.Type == "card_error")
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
if (_publicApi)
|
||||
{
|
||||
publicErrorModel = new ErrorResponseModel(stripeException.StripeError.Param,
|
||||
stripeException.Message);
|
||||
publicErrorModel = new ErrorResponseModel(badRequestException.ModelState);
|
||||
}
|
||||
else
|
||||
{
|
||||
internalErrorModel = new InternalApi.ErrorResponseModel(stripeException.StripeError.Param,
|
||||
stripeException.Message);
|
||||
internalErrorModel = new InternalApi.ErrorResponseModel(badRequestException.ModelState);
|
||||
}
|
||||
}
|
||||
else if (exception is GatewayException)
|
||||
{
|
||||
errorMessage = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if (exception is NotSupportedException && !string.IsNullOrWhiteSpace(exception.Message))
|
||||
{
|
||||
errorMessage = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if (exception is ApplicationException)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 402;
|
||||
}
|
||||
else if (exception is NotFoundException)
|
||||
{
|
||||
errorMessage = "Resource not found.";
|
||||
context.HttpContext.Response.StatusCode = 404;
|
||||
}
|
||||
else if (exception is SecurityTokenValidationException)
|
||||
{
|
||||
errorMessage = "Invalid token.";
|
||||
context.HttpContext.Response.StatusCode = 403;
|
||||
}
|
||||
else if (exception is UnauthorizedAccessException)
|
||||
{
|
||||
errorMessage = "Unauthorized.";
|
||||
context.HttpContext.Response.StatusCode = 401;
|
||||
}
|
||||
else
|
||||
{
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
||||
logger.LogError(0, exception, exception.Message);
|
||||
errorMessage = "An unhandled server error has occurred.";
|
||||
context.HttpContext.Response.StatusCode = 500;
|
||||
errorMessage = badRequestException.Message;
|
||||
}
|
||||
|
||||
}
|
||||
else if (exception is StripeException stripeException && stripeException?.StripeError?.Type == "card_error")
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
if (_publicApi)
|
||||
{
|
||||
var errorModel = publicErrorModel ?? new ErrorResponseModel(errorMessage);
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
publicErrorModel = new ErrorResponseModel(stripeException.StripeError.Param,
|
||||
stripeException.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorModel = internalErrorModel ?? new InternalApi.ErrorResponseModel(errorMessage);
|
||||
var env = context.HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
errorModel.ExceptionMessage = exception.Message;
|
||||
errorModel.ExceptionStackTrace = exception.StackTrace;
|
||||
errorModel.InnerExceptionMessage = exception?.InnerException?.Message;
|
||||
}
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
internalErrorModel = new InternalApi.ErrorResponseModel(stripeException.StripeError.Param,
|
||||
stripeException.Message);
|
||||
}
|
||||
}
|
||||
else if (exception is GatewayException)
|
||||
{
|
||||
errorMessage = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if (exception is NotSupportedException && !string.IsNullOrWhiteSpace(exception.Message))
|
||||
{
|
||||
errorMessage = exception.Message;
|
||||
context.HttpContext.Response.StatusCode = 400;
|
||||
}
|
||||
else if (exception is ApplicationException)
|
||||
{
|
||||
context.HttpContext.Response.StatusCode = 402;
|
||||
}
|
||||
else if (exception is NotFoundException)
|
||||
{
|
||||
errorMessage = "Resource not found.";
|
||||
context.HttpContext.Response.StatusCode = 404;
|
||||
}
|
||||
else if (exception is SecurityTokenValidationException)
|
||||
{
|
||||
errorMessage = "Invalid token.";
|
||||
context.HttpContext.Response.StatusCode = 403;
|
||||
}
|
||||
else if (exception is UnauthorizedAccessException)
|
||||
{
|
||||
errorMessage = "Unauthorized.";
|
||||
context.HttpContext.Response.StatusCode = 401;
|
||||
}
|
||||
else
|
||||
{
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
||||
logger.LogError(0, exception, exception.Message);
|
||||
errorMessage = "An unhandled server error has occurred.";
|
||||
context.HttpContext.Response.StatusCode = 500;
|
||||
}
|
||||
|
||||
if (_publicApi)
|
||||
{
|
||||
var errorModel = publicErrorModel ?? new ErrorResponseModel(errorMessage);
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorModel = internalErrorModel ?? new InternalApi.ErrorResponseModel(errorMessage);
|
||||
var env = context.HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
errorModel.ExceptionMessage = exception.Message;
|
||||
errorModel.ExceptionStackTrace = exception.StackTrace;
|
||||
errorModel.InnerExceptionMessage = exception?.InnerException?.Message;
|
||||
}
|
||||
context.Result = new ObjectResult(errorModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,27 +3,26 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using InternalApi = Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public class ModelStateValidationFilterAttribute : SharedWeb.Utilities.ModelStateValidationFilterAttribute
|
||||
{
|
||||
public class ModelStateValidationFilterAttribute : SharedWeb.Utilities.ModelStateValidationFilterAttribute
|
||||
private readonly bool _publicApi;
|
||||
|
||||
public ModelStateValidationFilterAttribute(bool publicApi)
|
||||
{
|
||||
private readonly bool _publicApi;
|
||||
_publicApi = publicApi;
|
||||
}
|
||||
|
||||
public ModelStateValidationFilterAttribute(bool publicApi)
|
||||
protected override void OnModelStateInvalid(ActionExecutingContext context)
|
||||
{
|
||||
if (_publicApi)
|
||||
{
|
||||
_publicApi = publicApi;
|
||||
context.Result = new BadRequestObjectResult(new ErrorResponseModel(context.ModelState));
|
||||
}
|
||||
|
||||
protected override void OnModelStateInvalid(ActionExecutingContext context)
|
||||
else
|
||||
{
|
||||
if (_publicApi)
|
||||
{
|
||||
context.Result = new BadRequestObjectResult(new ErrorResponseModel(context.ModelState));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Result = new BadRequestObjectResult(new InternalApi.ErrorResponseModel(context.ModelState));
|
||||
}
|
||||
context.Result = new BadRequestObjectResult(new InternalApi.ErrorResponseModel(context.ModelState));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,75 +5,41 @@ using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public static class MultipartFormDataHelper
|
||||
{
|
||||
public static class MultipartFormDataHelper
|
||||
private static readonly FormOptions _defaultFormOptions = new FormOptions();
|
||||
|
||||
public static async Task GetFileAsync(this HttpRequest request, Func<Stream, string, string, Task> callback)
|
||||
{
|
||||
private static readonly FormOptions _defaultFormOptions = new FormOptions();
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
var reader = new MultipartReader(boundary, request.Body);
|
||||
|
||||
public static async Task GetFileAsync(this HttpRequest request, Func<Stream, string, string, Task> callback)
|
||||
var firstSection = await reader.ReadNextSectionAsync();
|
||||
if (firstSection != null)
|
||||
{
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
var reader = new MultipartReader(boundary, request.Body);
|
||||
|
||||
var firstSection = await reader.ReadNextSectionAsync();
|
||||
if (firstSection != null)
|
||||
if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out var firstContent))
|
||||
{
|
||||
if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out var firstContent))
|
||||
if (HasFileContentDisposition(firstContent))
|
||||
{
|
||||
if (HasFileContentDisposition(firstContent))
|
||||
// Old style with just data
|
||||
var fileName = HeaderUtilities.RemoveQuotes(firstContent.FileName).ToString();
|
||||
using (firstSection.Body)
|
||||
{
|
||||
// Old style with just data
|
||||
var fileName = HeaderUtilities.RemoveQuotes(firstContent.FileName).ToString();
|
||||
using (firstSection.Body)
|
||||
{
|
||||
await callback(firstSection.Body, fileName, null);
|
||||
}
|
||||
}
|
||||
else if (HasDispositionName(firstContent, "key"))
|
||||
{
|
||||
// New style with key, then data
|
||||
string key = null;
|
||||
using (var sr = new StreamReader(firstSection.Body))
|
||||
{
|
||||
key = await sr.ReadToEndAsync();
|
||||
}
|
||||
|
||||
var secondSection = await reader.ReadNextSectionAsync();
|
||||
if (secondSection != null)
|
||||
{
|
||||
if (ContentDispositionHeaderValue.TryParse(secondSection.ContentDisposition,
|
||||
out var secondContent) && HasFileContentDisposition(secondContent))
|
||||
{
|
||||
var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString();
|
||||
using (secondSection.Body)
|
||||
{
|
||||
await callback(secondSection.Body, fileName, key);
|
||||
}
|
||||
}
|
||||
|
||||
secondSection = null;
|
||||
}
|
||||
await callback(firstSection.Body, fileName, null);
|
||||
}
|
||||
}
|
||||
|
||||
firstSection = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, string,
|
||||
SendRequestModel, Task> callback)
|
||||
{
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
var reader = new MultipartReader(boundary, request.Body);
|
||||
|
||||
var firstSection = await reader.ReadNextSectionAsync();
|
||||
if (firstSection != null)
|
||||
{
|
||||
if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out _))
|
||||
else if (HasDispositionName(firstContent, "key"))
|
||||
{
|
||||
// New style with key, then data
|
||||
string key = null;
|
||||
using (var sr = new StreamReader(firstSection.Body))
|
||||
{
|
||||
key = await sr.ReadToEndAsync();
|
||||
}
|
||||
|
||||
var secondSection = await reader.ReadNextSectionAsync();
|
||||
if (secondSection != null)
|
||||
{
|
||||
@ -83,69 +49,102 @@ namespace Bit.Api.Utilities
|
||||
var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString();
|
||||
using (secondSection.Body)
|
||||
{
|
||||
var model = await JsonSerializer.DeserializeAsync<SendRequestModel>(firstSection.Body);
|
||||
await callback(secondSection.Body, fileName, model);
|
||||
await callback(secondSection.Body, fileName, key);
|
||||
}
|
||||
}
|
||||
|
||||
secondSection = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
firstSection = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GetFileAsync(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)
|
||||
{
|
||||
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
|
||||
if (StringSegment.IsNullOrEmpty(boundary))
|
||||
{
|
||||
throw new InvalidDataException("Missing content-type boundary.");
|
||||
}
|
||||
|
||||
if (boundary.Length > lengthLimit)
|
||||
{
|
||||
throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
|
||||
}
|
||||
|
||||
return boundary.ToString();
|
||||
}
|
||||
|
||||
private static bool HasFileContentDisposition(ContentDispositionHeaderValue content)
|
||||
{
|
||||
// Content-Disposition: form-data; name="data"; filename="Misc 002.jpg"
|
||||
return content != null && content.DispositionType.Equals("form-data") &&
|
||||
(!StringSegment.IsNullOrEmpty(content.FileName) || !StringSegment.IsNullOrEmpty(content.FileNameStar));
|
||||
}
|
||||
|
||||
private static bool HasDispositionName(ContentDispositionHeaderValue content, string name)
|
||||
{
|
||||
// Content-Disposition: form-data; name="key";
|
||||
return content != null && content.DispositionType.Equals("form-data") && content.Name == name;
|
||||
firstSection = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, string,
|
||||
SendRequestModel, Task> callback)
|
||||
{
|
||||
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
|
||||
_defaultFormOptions.MultipartBoundaryLengthLimit);
|
||||
var reader = new MultipartReader(boundary, request.Body);
|
||||
|
||||
var firstSection = await reader.ReadNextSectionAsync();
|
||||
if (firstSection != null)
|
||||
{
|
||||
if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out _))
|
||||
{
|
||||
var secondSection = await reader.ReadNextSectionAsync();
|
||||
if (secondSection != null)
|
||||
{
|
||||
if (ContentDispositionHeaderValue.TryParse(secondSection.ContentDisposition,
|
||||
out var secondContent) && HasFileContentDisposition(secondContent))
|
||||
{
|
||||
var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString();
|
||||
using (secondSection.Body)
|
||||
{
|
||||
var model = await JsonSerializer.DeserializeAsync<SendRequestModel>(firstSection.Body);
|
||||
await callback(secondSection.Body, fileName, model);
|
||||
}
|
||||
}
|
||||
|
||||
secondSection = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
firstSection = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task GetFileAsync(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)
|
||||
{
|
||||
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
|
||||
if (StringSegment.IsNullOrEmpty(boundary))
|
||||
{
|
||||
throw new InvalidDataException("Missing content-type boundary.");
|
||||
}
|
||||
|
||||
if (boundary.Length > lengthLimit)
|
||||
{
|
||||
throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
|
||||
}
|
||||
|
||||
return boundary.ToString();
|
||||
}
|
||||
|
||||
private static bool HasFileContentDisposition(ContentDispositionHeaderValue content)
|
||||
{
|
||||
// Content-Disposition: form-data; name="data"; filename="Misc 002.jpg"
|
||||
return content != null && content.DispositionType.Equals("form-data") &&
|
||||
(!StringSegment.IsNullOrEmpty(content.FileName) || !StringSegment.IsNullOrEmpty(content.FileNameStar));
|
||||
}
|
||||
|
||||
private static bool HasDispositionName(ContentDispositionHeaderValue content, string name)
|
||||
{
|
||||
// Content-Disposition: form-data; name="key";
|
||||
return content != null && content.DispositionType.Equals("form-data") && content.Name == name;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public class PublicApiControllersModelConvention : IControllerModelConvention
|
||||
{
|
||||
public class PublicApiControllersModelConvention : IControllerModelConvention
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
var controllerNamespace = controller.ControllerType.Namespace;
|
||||
var publicApi = controllerNamespace.Contains(".Public.");
|
||||
controller.Filters.Add(new ExceptionHandlerFilterAttribute(publicApi));
|
||||
controller.Filters.Add(new ModelStateValidationFilterAttribute(publicApi));
|
||||
}
|
||||
var controllerNamespace = controller.ControllerType.Namespace;
|
||||
var publicApi = controllerNamespace.Contains(".Public.");
|
||||
controller.Filters.Add(new ExceptionHandlerFilterAttribute(publicApi));
|
||||
controller.Filters.Add(new ModelStateValidationFilterAttribute(publicApi));
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class SecretsManagerAttribute : Attribute, IResourceFilter
|
||||
{
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class SecretsManagerAttribute : Attribute, IResourceFilter
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
{
|
||||
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||
var env = context.HttpContext.RequestServices.GetService<IHostEnvironment>();
|
||||
if (!env.IsDevelopment())
|
||||
{
|
||||
var env = context.HttpContext.RequestServices.GetService<IHostEnvironment>();
|
||||
if (!env.IsDevelopment())
|
||||
{
|
||||
context.Result = new NotFoundResult();
|
||||
}
|
||||
context.Result = new NotFoundResult();
|
||||
}
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context) { }
|
||||
}
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context) { }
|
||||
}
|
||||
|
||||
|
@ -1,72 +1,71 @@
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Bit.Api.Utilities
|
||||
namespace Bit.Api.Utilities;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
{
|
||||
public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
services.AddSwaggerGen(config =>
|
||||
{
|
||||
services.AddSwaggerGen(config =>
|
||||
config.SwaggerDoc("public", new OpenApiInfo
|
||||
{
|
||||
config.SwaggerDoc("public", new OpenApiInfo
|
||||
Title = "Bitwarden Public API",
|
||||
Version = "latest",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Title = "Bitwarden Public API",
|
||||
Version = "latest",
|
||||
Contact = new OpenApiContact
|
||||
{
|
||||
Name = "Bitwarden Support",
|
||||
Url = new Uri("https://bitwarden.com"),
|
||||
Email = "support@bitwarden.com"
|
||||
},
|
||||
Description = "The Bitwarden public APIs.",
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
Name = "GNU Affero General Public License v3.0",
|
||||
Url = new Uri("https://github.com/bitwarden/server/blob/master/LICENSE.txt")
|
||||
}
|
||||
});
|
||||
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
||||
|
||||
config.AddSecurityDefinition("OAuth2 Client Credentials", new OpenApiSecurityScheme
|
||||
Name = "Bitwarden Support",
|
||||
Url = new Uri("https://bitwarden.com"),
|
||||
Email = "support@bitwarden.com"
|
||||
},
|
||||
Description = "The Bitwarden public APIs.",
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ "api.organization", "Organization APIs" },
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
config.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "OAuth2 Client Credentials"
|
||||
},
|
||||
},
|
||||
new[] { "api.organization" }
|
||||
}
|
||||
});
|
||||
|
||||
config.DescribeAllParametersInCamelCase();
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
||||
config.IncludeXmlComments(apiFilePath, true);
|
||||
var coreFilePath = Path.Combine(AppContext.BaseDirectory, "Core.xml");
|
||||
config.IncludeXmlComments(coreFilePath);
|
||||
Name = "GNU Affero General Public License v3.0",
|
||||
Url = new Uri("https://github.com/bitwarden/server/blob/master/LICENSE.txt")
|
||||
}
|
||||
});
|
||||
}
|
||||
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
||||
|
||||
config.AddSecurityDefinition("OAuth2 Client Credentials", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
ClientCredentials = new OpenApiOAuthFlow
|
||||
{
|
||||
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ "api.organization", "Organization APIs" },
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
config.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "OAuth2 Client Credentials"
|
||||
},
|
||||
},
|
||||
new[] { "api.organization" }
|
||||
}
|
||||
});
|
||||
|
||||
config.DescribeAllParametersInCamelCase();
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
||||
config.IncludeXmlComments(apiFilePath, true);
|
||||
var coreFilePath = Path.Combine(AppContext.BaseDirectory, "Core.xml");
|
||||
config.IncludeXmlComments(coreFilePath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user