-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathRequestValidator.cs
More file actions
165 lines (139 loc) · 6.69 KB
/
RequestValidator.cs
File metadata and controls
165 lines (139 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.OpenApi.Models;
namespace Swashbuckle.AspNetCore.ApiTesting
{
public class RequestValidator(IEnumerable<IContentValidator> contentValidators)
{
private readonly IEnumerable<IContentValidator> _contentValidators = contentValidators;
public void Validate(
HttpRequestMessage request,
OpenApiDocument openApiDocument,
string pathTemplate,
OperationType operationType)
{
var operationSpec = openApiDocument.GetOperationByPathAndType(pathTemplate, operationType, out OpenApiPathItem pathSpec);
var parameterSpecs = ExpandParameterSpecs(pathSpec, operationSpec, openApiDocument);
// Convert to absolute Uri as a workaround to limitation with Uri class - i.e. most of it's methods are not supported for relative Uri's.
var requestUri = new Uri(new Uri("http://tempuri.org"), request.RequestUri);
if (!TryParsePathNameValues(pathTemplate, requestUri.AbsolutePath, out NameValueCollection pathNameValues))
{
throw new RequestDoesNotMatchSpecException($"Request URI '{requestUri.AbsolutePath}' does not match specified template '{pathTemplate}'");
}
if (request.Method != new HttpMethod(operationType.ToString()))
{
throw new RequestDoesNotMatchSpecException($"Request method '{request.Method}' does not match specified operation type '{operationType}'");
}
ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Path), openApiDocument, pathNameValues);
ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Query), openApiDocument, HttpUtility.ParseQueryString(requestUri.Query));
ValidateParameters(parameterSpecs.Where(p => p.In == ParameterLocation.Header), openApiDocument, request.Headers.ToNameValueCollection());
if (operationSpec.RequestBody != null)
{
ValidateContent(operationSpec.RequestBody, openApiDocument, request.Content);
}
}
private static IEnumerable<OpenApiParameter> ExpandParameterSpecs(
OpenApiPathItem pathSpec,
OpenApiOperation operationSpec,
OpenApiDocument openApiDocument)
{
var securityParameterSpecs = DeriveSecurityParameterSpecs(operationSpec, openApiDocument);
return securityParameterSpecs
.Concat(pathSpec.Parameters)
.Concat(operationSpec.Parameters)
.Select(p =>
{
return p.Reference != null ?
(OpenApiParameter)openApiDocument.ResolveReference(p.Reference)
: p;
});
}
private static IEnumerable<OpenApiParameter> DeriveSecurityParameterSpecs(
OpenApiOperation operationSpec,
OpenApiDocument openApiDocument)
{
// TODO
return [];
}
private static bool TryParsePathNameValues(string pathTemplate, string requestUri, out NameValueCollection pathNameValues)
{
pathNameValues = [];
var templateMatcher = new TemplateMatcher(TemplateParser.Parse(pathTemplate), null);
var routeValues = new RouteValueDictionary();
if (!templateMatcher.TryMatch(new PathString(requestUri), routeValues))
{
return false;
}
foreach (var entry in routeValues)
{
pathNameValues.Add(entry.Key, entry.Value.ToString());
}
return true;
}
private static void ValidateParameters(
IEnumerable<OpenApiParameter> parameterSpecs,
OpenApiDocument openApiDocument,
NameValueCollection parameterNameValues)
{
foreach (var parameterSpec in parameterSpecs)
{
var value = parameterNameValues[parameterSpec.Name];
if ((parameterSpec.In == ParameterLocation.Path || parameterSpec.Required) && value == null)
{
throw new RequestDoesNotMatchSpecException($"Required parameter '{parameterSpec.Name}' is not present");
}
if (value == null || parameterSpec.Schema == null)
{
continue;
}
var schema = (parameterSpec.Schema.Reference != null) ?
(OpenApiSchema)openApiDocument.ResolveReference(parameterSpec.Schema.Reference)
: parameterSpec.Schema;
if (!schema.TryParse(value, out object typedValue))
{
throw new RequestDoesNotMatchSpecException($"Parameter '{parameterSpec.Name}' is not of type '{parameterSpec.Schema.TypeIdentifier()}'");
}
}
}
private void ValidateContent(OpenApiRequestBody requestBodySpec, OpenApiDocument openApiDocument, HttpContent content)
{
requestBodySpec = requestBodySpec.Reference != null ?
(OpenApiRequestBody)openApiDocument.ResolveReference(requestBodySpec.Reference)
: requestBodySpec;
if (requestBodySpec.Required && content == null)
{
throw new RequestDoesNotMatchSpecException("Required content is not present");
}
if (content == null)
{
return;
}
if (!requestBodySpec.Content.TryGetValue(content.Headers.ContentType.MediaType, out OpenApiMediaType mediaTypeSpec))
{
throw new RequestDoesNotMatchSpecException($"Content media type '{content.Headers.ContentType.MediaType}' is not specified");
}
try
{
foreach (var contentValidator in _contentValidators)
{
if (contentValidator.CanValidate(content.Headers.ContentType.MediaType))
{
contentValidator.Validate(mediaTypeSpec, openApiDocument, content);
}
}
}
catch (ContentDoesNotMatchSpecException contentException)
{
throw new RequestDoesNotMatchSpecException($"Content does not match spec. {contentException.Message}");
}
}
}
public class RequestDoesNotMatchSpecException(string message) : Exception(message);
}