Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attribute for media type validation #1117

Merged
merged 11 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion samples/Samples.Upload/Mutation.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using GraphQL;
using GraphQL.Server.Transports.AspNetCore;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;

namespace Samples.Upload;

public class Mutation
{
public static async Task<string> Rotate(IFormFile file, CancellationToken cancellationToken)
public static async Task<string> Rotate([MediaType("image/*")] IFormFile file, CancellationToken cancellationToken)
{
if (file == null || file.Length == 0)
{
Expand Down
136 changes: 136 additions & 0 deletions src/Transports.AspNetCore/MediaTypeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Collections;
using MediaTypeHeaderValue = Microsoft.Net.Http.Headers.MediaTypeHeaderValue;

namespace GraphQL.Server.Transports.AspNetCore;

/// <summary>
/// Ensures that the marked field argument or input object field is a valid media type
/// via the <see cref="MediaTypeHeaderValue.IsSubsetOf(MediaTypeHeaderValue)"/> method
/// and supports wildcards such as "<c>text/*</c>".
/// </summary>
/// <remarks>
/// Only checks values of type <see cref="IFormFile"/>, or lists of <see cref="IFormFile"/>.
/// Any other types of values will throw a run-time exception.
/// </remarks>
public class MediaTypeAttribute : GraphQLAttribute
{
private readonly MediaTypeHeaderValue[] _mimeTypes;

/// <inheritdoc cref="MediaTypeAttribute"/>
public MediaTypeAttribute(params string[] mimeTypes)
{
var types = MediaTypeHeaderValue.ParseList(mimeTypes);
_mimeTypes = types as MediaTypeHeaderValue[] ?? types.ToArray();
}

/// <inheritdoc/>
public override void Modify(FieldType fieldType, bool isInputType)
{
var lists = fieldType.Type != null
? CountNestedLists(fieldType.Type)
: CountNestedLists(fieldType.ResolvedType
?? throw new InvalidOperationException($"No graph type set on field '{fieldType.Name}'."));
fieldType.Validator += new Validator(lists, _mimeTypes).Validate;
}

/// <inheritdoc/>
public override void Modify(QueryArgument queryArgument)
{
var lists = queryArgument.Type != null
? CountNestedLists(queryArgument.Type)
: CountNestedLists(queryArgument.ResolvedType
?? throw new InvalidOperationException($"No graph type set on field '{queryArgument.Name}'."));
queryArgument.Validate(new Validator(lists, _mimeTypes).Validate);
}

private class Validator
{
private readonly int _lists;
private readonly MediaTypeHeaderValue[] _mediaTypes;

public Validator(int lists, MediaTypeHeaderValue[] mediaTypes)
{
_lists = lists;
_mediaTypes = mediaTypes;
}

public void Validate(object? obj)
{
Validate(obj, _lists);
}

public void Validate(object? obj, int lists)
{
if (obj == null)
return;
if (lists == 0)
{
if (obj is IFormFile file)
ValidateMediaType(file);
else
throw new InvalidOperationException("Expected an IFormFile object.");
}
else if (obj is IEnumerable enumerable)
Shane32 marked this conversation as resolved.
Show resolved Hide resolved
{
foreach (var item in enumerable)
{
Validate(item, lists - 1);
}
}
else
{
throw new InvalidOperationException("Expected a list.");
}
}

public void ValidateMediaType(IFormFile? file)
{
if (file == null)
return;
var contentType = file.ContentType;
if (contentType == null)
return;
var mediaType = MediaTypeHeaderValue.Parse(contentType);
foreach (var validMediaType in _mediaTypes)
{
if (mediaType.IsSubsetOf(validMediaType))
return;
}
Dismissed Show dismissed Hide dismissed
throw new InvalidOperationException($"Invalid media type '{mediaType}'.");
}
}
private static int CountNestedLists(Type type)
{
if (!type.IsGenericType)
return 0;

var typeDef = type.GetGenericTypeDefinition();

if (typeDef == typeof(ListGraphType<>))
{
return 1 + CountNestedLists(type.GetGenericArguments()[0]);
}

if (typeDef == typeof(NonNullGraphType<>))
{
return CountNestedLists(type.GetGenericArguments()[0]);
}

return 0;
}

private static int CountNestedLists(IGraphType type)
{
if (type is ListGraphType listGraphType)
{
return 1 + CountNestedLists(listGraphType.ResolvedType ?? throw new InvalidOperationException($"ResolvedType not set for {listGraphType}."));
}

if (type is NonNullGraphType nonNullGraphType)
{
return CountNestedLists(nonNullGraphType.ResolvedType ?? throw new InvalidOperationException($"ResolvedType not set for {nonNullGraphType}."));
}

return 0;
}
}
2 changes: 1 addition & 1 deletion src/Transports.AspNetCore/Transports.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="GraphQL.MicrosoftDI" Version="$(GraphQLVersion)" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="8.0.0-preview-975" />
Shane32 marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0' AND '$(TargetFramework)' != 'netcoreapp2.1'">
Expand Down
33 changes: 33 additions & 0 deletions tests/Samples.Upload.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,37 @@ public async Task RotateImage()
var ret = await response.Content.ReadAsStringAsync();
ret.ShouldBe("{\"data\":{\"rotate\":\"/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDIBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIACAAIAMBIgACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5\\u002BgEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4\\u002BTl5ufo6ery8/T19vf4\\u002Bfr/2gAMAwEAAhEDEQA/APUNa8c6boGvppeorLFG8Kyi5UblXlgQwHzdh0B69sZrorS8tr\\u002B2We0uY54X\\u002B7LGwZWwcHBHHWvGfi\\u002Bf\\u002BKwh5z/oSf8Aob1x\\u002Bla1qOiXP2jTbuS3kPXbyrcEDKng4ycZBxXnzxjp1ZRkro\\u002BsocNxxeCp16MuWbV3fVPf5r8UfUGSME8etDMBk\\u002BleX\\u002BH/AIt2twyQ65AbZ\\u002Bc3EILRnqeV\\u002B8Ow43ZPoK9HtLy2v7ZLi0njmhfO2SNtynBxwRx1rtp1YVFeLPncZgMTg5cteDXn0fo9v62PGPi//wAjhB/15J/6G9cLb2093OsFtBLNM2cRxoXZsDJwBz0r3DxL8P18UeJYr\\u002B7vGgtEt1i8uJcyMwLk8ngDlexzz0610mlaFpmgW7R6bZRwI33ivLNgkjcx5OMnGelcE8HKpVlJ6K59RhuIqODwFOjBc00vRLfd/wCX3nmGg/CS\\u002BugJdcn\\u002ByJ/zxhIeQ9Ry3QdjxuyPQ16fpXh/TNBhaLTLOO3VsZK8s2CSNzHk4ycZ6VsU0Z9K7aVCFL4UfO47NcVjn\\u002B\\u002Blp2Wi\\u002B7r87n//2Q==\"}}");
}

[Fact]
public async Task RotateImage_WrongType()
{
using var webApp = new WebApplicationFactory<Program>();
var server = webApp.Server;

using var client = server.CreateClient();
var form = new MultipartFormDataContent();
var operations = new
{
query = "mutation ($img: FormFile!) { rotate(file: $img) }",
variables = new { img = (string?)null },
Dismissed Show dismissed Hide dismissed
};
form.Add(JsonContent.Create(operations), "operations");
var map = new
{
file0 = new string[] { "variables.img" },
};
form.Add(JsonContent.Create(map), "map");
// base 64 of hello world
var base64hello = "aGVsbG8gd29ybGQ=";
var triangle = Convert.FromBase64String(base64hello);
var triangleContent = new ByteArrayContent(triangle);
triangleContent.Headers.ContentType = new("text/text");
form.Add(triangleContent, "file0", "hello-world.txt");
using var request = new HttpRequestMessage(HttpMethod.Post, "/graphql");
request.Content = form;
using var response = await client.SendAsync(request);
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
var ret = await response.Content.ReadAsStringAsync();
ret.ShouldBe("{\"errors\":[{\"message\":\"Invalid value for argument \\u0027file\\u0027 of field \\u0027rotate\\u0027. Invalid media type \\u0027text/text\\u0027.\",\"locations\":[{\"line\":1,\"column\":43}],\"extensions\":{\"code\":\"INVALID_VALUE\",\"codes\":[\"INVALID_VALUE\",\"INVALID_OPERATION\"],\"number\":\"5.6\"}}]}");
}
}
Loading