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

Integrate http binding with validators #571

Merged
merged 56 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
df07f72
Revert "Add http request validators feature flag (#472)"
attilakreiner Sep 28, 2023
f3e2245
WIP
attilakreiner Sep 28, 2023
7691312
fix
attilakreiner Oct 10, 2023
1f811c4
WIP
attilakreiner Oct 11, 2023
502db7f
WIP integation tests
attilakreiner Oct 13, 2023
330119d
WIP integration tests
attilakreiner Oct 16, 2023
2963d52
WIP h2 tests
attilakreiner Oct 18, 2023
4d9a19b
WIP h2 tests - content
attilakreiner Oct 19, 2023
dce5c70
WIP http/1.1 ITs - content
attilakreiner Oct 20, 2023
5e11f6f
fix
attilakreiner Oct 20, 2023
16af5e3
fix
attilakreiner Oct 21, 2023
4452d5b
WIP
attilakreiner Oct 25, 2023
eb9842b
fix method names
attilakreiner Oct 26, 2023
09e3087
WIP fix
attilakreiner Oct 26, 2023
e6ea82c
cleanup
attilakreiner Oct 26, 2023
dfcc7fa
fix h2 invalid test so all requests use the same connection
attilakreiner Oct 26, 2023
9a7f3c3
fix review items 1
attilakreiner Oct 27, 2023
215e72c
fix review items 2
attilakreiner Oct 27, 2023
cb5ffd8
fix review items 3
attilakreiner Oct 27, 2023
e95c006
Add server side for the invalid test h2
attilakreiner Oct 27, 2023
1296060
Fix invalid content in http/1.1 validation
attilakreiner Nov 2, 2023
769643b
Fix duplicate response header issue
attilakreiner Nov 2, 2023
a86eef4
fix NPE
attilakreiner Nov 3, 2023
345ded8
Revert "Fix duplicate response header issue"
attilakreiner Nov 3, 2023
9ffb3b1
send reset frame
attilakreiner Nov 3, 2023
7ee5980
fix
attilakreiner Nov 3, 2023
f57273f
fix 1
attilakreiner Nov 6, 2023
ad0c98f
fix 2
attilakreiner Nov 6, 2023
b5eded0
fix 3
attilakreiner Nov 6, 2023
ba32eb1
fix
attilakreiner Nov 7, 2023
3b4af62
fix 1
attilakreiner Nov 8, 2023
f3f0ee2
fix 2
attilakreiner Nov 8, 2023
5f6f544
fix 3
attilakreiner Nov 8, 2023
60d2ed5
fix 4
attilakreiner Nov 8, 2023
1cb75af
fix 5
attilakreiner Nov 8, 2023
0b5b33e
fix 6
attilakreiner Nov 8, 2023
d0ce297
fix 7
attilakreiner Nov 8, 2023
69812f7
fix 8
attilakreiner Nov 8, 2023
187e877
fix 9
attilakreiner Nov 8, 2023
cccd57f
fix 10
attilakreiner Nov 8, 2023
1b5a811
refactor HttpRequestType
attilakreiner Nov 8, 2023
3a5c307
refactor HttpRequestType 2
attilakreiner Nov 8, 2023
d748cca
WIP TreeMap
attilakreiner Nov 8, 2023
fc290e7
Add unit tests for urlDecodedComparator
attilakreiner Nov 8, 2023
dce06c6
fix 1
attilakreiner Nov 9, 2023
662fd09
fix 2
attilakreiner Nov 9, 2023
decd91d
fix 3
attilakreiner Nov 9, 2023
d41768f
PercentEncodableStringComparator 1
attilakreiner Nov 9, 2023
99718e1
PercentEncodableStringComparator 2
attilakreiner Nov 9, 2023
115da84
fix 4
attilakreiner Nov 9, 2023
be01927
WIP fix bug
attilakreiner Nov 10, 2023
b553ebe
fix 1
attilakreiner Nov 10, 2023
5cc0499
fix 2
attilakreiner Nov 10, 2023
0acab22
fix 1
attilakreiner Nov 11, 2023
12728d4
fix 2
attilakreiner Nov 13, 2023
d5e567c
fix 3
attilakreiner Nov 13, 2023
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
2 changes: 1 addition & 1 deletion runtime/binding-http/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
</fileMappers>
</artifactItem>
</artifactItems>
<includes>io/aklivity/zilla/specs/binding/http/schema/http*.schema.patch.json</includes>
<includes>io/aklivity/zilla/specs/binding/http/schema/http.schema.patch.json</includes>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ public enum Method
TRACE
}

public String path;
public Method method;
public List<String> contentType;
public List<HttpParamConfig> headers;
public List<HttpParamConfig> pathParams;
public List<HttpParamConfig> queryParams;
public ValidatorConfig content;
public final String path;
public final Method method;
public final List<String> contentType;
public final List<HttpParamConfig> headers;
public final List<HttpParamConfig> pathParams;
public final List<HttpParamConfig> queryParams;
public final ValidatorConfig content;

public HttpRequestConfig(
String path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ public String name()
@Override
public URL type()
{
String patch = config.requestValidators()
? "schema/http.with.validators.schema.patch.json"
: "schema/http.schema.patch.json";
return getClass().getResource(patch);
return getClass().getResource("schema/http.schema.patch.json");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public class HttpConfiguration extends Configuration
public static final IntPropertyDef HTTP_MAX_CONCURRENT_APPLICATION_HEADERS;
public static final PropertyDef<String> HTTP_SERVER_HEADER;
public static final PropertyDef<String> HTTP_USER_AGENT_HEADER;
public static final BooleanPropertyDef HTTP_REQUEST_VALIDATORS;

private static final ConfigurationDef HTTP_CONFIG;

Expand All @@ -53,7 +52,6 @@ public class HttpConfiguration extends Configuration
HTTP_MAX_CONCURRENT_STREAMS_CLEANUP = config.property("max.concurrent.streams.cleanup", 1000);
HTTP_STREAMS_CLEANUP_DELAY = config.property("streams.cleanup.delay", 100);
HTTP_MAX_CONCURRENT_APPLICATION_HEADERS = config.property("max.concurrent.application.headers", 10000);
HTTP_REQUEST_VALIDATORS = config.property("request.validators", false);
HTTP_CONFIG = config;
}

Expand Down Expand Up @@ -115,11 +113,6 @@ public int maxConcurrentApplicationHeaders()
return HTTP_MAX_CONCURRENT_APPLICATION_HEADERS.getAsInt(this);
}

public boolean requestValidators()
{
return HTTP_REQUEST_VALIDATORS.getAsBoolean(this);
}

public String16FW serverHeader()
{
return serverHeader;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,49 @@
import static java.util.EnumSet.allOf;
import static java.util.stream.Collectors.toList;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.ToLongFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.agrona.DirectBuffer;
import org.agrona.collections.MutableBoolean;
import org.agrona.collections.Object2ObjectHashMap;

import io.aklivity.zilla.runtime.binding.http.config.HttpAccessControlConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpCredentialsConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpOptionsConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpParamConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpPatternConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpRequestConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpVersion;
import io.aklivity.zilla.runtime.binding.http.internal.types.HttpHeaderFW;
import io.aklivity.zilla.runtime.binding.http.internal.types.String16FW;
import io.aklivity.zilla.runtime.binding.http.internal.types.String8FW;
import io.aklivity.zilla.runtime.binding.http.internal.types.stream.HttpBeginExFW;
import io.aklivity.zilla.runtime.engine.config.BindingConfig;
import io.aklivity.zilla.runtime.engine.config.KindConfig;
import io.aklivity.zilla.runtime.engine.config.ValidatorConfig;
import io.aklivity.zilla.runtime.engine.validator.Validator;

public final class HttpBindingConfig
{
private static final Function<Function<String, String>, String> DEFAULT_CREDENTIALS = f -> null;
private static final SortedSet<HttpVersion> DEFAULT_VERSIONS = new TreeSet<>(allOf(HttpVersion.class));
private static final HttpAccessControlConfig DEFAULT_ACCESS_CONTROL =
HttpAccessControlConfig.builder().policy(SAME_ORIGIN).build();
private static final String8FW HEADER_CONTENT_TYPE = new String8FW("content-type");
private static final String8FW HEADER_METHOD = new String8FW(":method");
private static final String8FW HEADER_PATH = new String8FW(":path");
private static final HttpQueryStringComparator QUERY_STRING_COMPARATOR = new HttpQueryStringComparator();

public final long id;
public final String name;
Expand All @@ -49,9 +70,17 @@ public final class HttpBindingConfig
public final List<HttpRouteConfig> routes;
public final ToLongFunction<String> resolveId;
public final Function<Function<String, String>, String> credentials;
public final List<HttpRequestType> requests;

public HttpBindingConfig(
BindingConfig binding)
{
this(binding, null);
}

public HttpBindingConfig(
BindingConfig binding,
BiFunction<ValidatorConfig, ToLongFunction<String>, Validator> createValidator)
{
this.id = binding.id;
this.name = binding.name;
Expand All @@ -61,6 +90,7 @@ public HttpBindingConfig(
this.resolveId = binding.resolveId;
this.credentials = options != null && options.authorization != null ?
asAccessor(options.authorization.credentials) : DEFAULT_CREDENTIALS;
this.requests = createValidator == null ? null : createRequestTypes(createValidator);
}

public HttpRouteConfig resolve(
Expand Down Expand Up @@ -164,6 +194,188 @@ private Function<Function<String, String>, String> asAccessor(
return accessor;
}

private List<HttpRequestType> createRequestTypes(
BiFunction<ValidatorConfig, ToLongFunction<String>, Validator> createValidator)
{
List<HttpRequestType> requestTypes = new LinkedList<>();
if (this.options != null && this.options.requests != null)
{
for (HttpRequestConfig request : this.options.requests)
{
Map<String8FW, Validator> headers = new HashMap<>();
if (request.headers != null)
{
for (HttpParamConfig header : request.headers)
{
headers.put(new String8FW(header.name), createValidator.apply(header.validator, this.resolveId));
}
}
Map<String, Validator> pathParams = new Object2ObjectHashMap<>();
if (request.pathParams != null)
{
for (HttpParamConfig pathParam : request.pathParams)
{
pathParams.put(pathParam.name, createValidator.apply(pathParam.validator, this.resolveId));
}
}
Map<String, Validator> queryParams = new TreeMap<>(QUERY_STRING_COMPARATOR);
if (request.queryParams != null)
{
for (HttpParamConfig queryParam : request.queryParams)
{
queryParams.put(queryParam.name, createValidator.apply(queryParam.validator, this.resolveId));
}
}
Validator content = createValidator.apply(request.content, this.resolveId);
HttpRequestType requestType = HttpRequestType.builder()
.path(request.path)
.method(request.method)
.contentType(request.contentType)
.headers(headers)
.pathParams(pathParams)
.queryParams(queryParams)
.content(content)
.build();
requestTypes.add(requestType);
}
}
return requestTypes;
}

public HttpRequestType resolveRequestType(
HttpBeginExFW beginEx)
{
HttpRequestType result = null;
if (requests != null && !requests.isEmpty())
{
String path = resolveHeaderValue(beginEx, HEADER_PATH);
String method = resolveHeaderValue(beginEx, HEADER_METHOD);
String contentType = resolveHeaderValue(beginEx, HEADER_CONTENT_TYPE);
for (HttpRequestType requestType : requests)
{
if (matchMethod(requestType, method) &&
matchContentType(requestType, contentType) &&
matchPath(requestType, path))
{
result = requestType;
break;
}
}
}
return result;
}

private boolean matchMethod(
HttpRequestType requestType,
String method)
{
return method == null || requestType.method == null || method.equals(requestType.method.name());
}

private boolean matchContentType(
HttpRequestType requestType,
String contentType)
{
return contentType == null || requestType.contentType == null || requestType.contentType.contains(contentType);
}

private boolean matchPath(
HttpRequestType requestType,
String path)
{
return requestType.pathMatcher.reset(path).matches();
}

public boolean validateHeaders(
HttpRequestType requestType,
HttpBeginExFW beginEx)
{
String path = beginEx.headers().matchFirst(h -> h.name().equals(HEADER_PATH)).value().asString();
return requestType == null ||
validateHeaderValues(requestType, beginEx) &&
validatePathParams(requestType, path) &&
validateQueryParams(requestType, path);
}

private boolean validateHeaderValues(
HttpRequestType requestType,
HttpBeginExFW beginEx)
{
MutableBoolean valid = new MutableBoolean(true);
if (requestType != null && requestType.headers != null)
{
beginEx.headers().forEach(header ->
{
if (valid.value)
{
Validator validator = requestType.headers.get(header.name());
if (validator != null)
{
String16FW value = header.value();
valid.value &= validator.read(value.value(), value.offset(), value.length());
}
}
});
}
return valid.value;
}

private boolean validatePathParams(
HttpRequestType requestType,
String path)
{
Matcher matcher = requestType.pathMatcher.reset(path);
boolean matches = matcher.matches();
assert matches;

boolean valid = true;
for (String name : requestType.pathParams.keySet())
{
String value = matcher.group(name);
if (value != null)
{
String8FW value0 = new String8FW(value);
Validator validator = requestType.pathParams.get(name);
if (!validator.read(value0.value(), value0.offset(), value0.length()))
{
valid = false;
break;
}
}
}
return valid;
}

private boolean validateQueryParams(
HttpRequestType requestType,
String path)
{
Matcher matcher = requestType.queryMatcher.reset(path);
boolean valid = true;
while (valid && matcher.find())
{
String name = matcher.group(1);
Validator validator = requestType.queryParams.get(name);
if (validator != null)
{
String8FW value = new String8FW(matcher.group(2));
valid &= validator.read(value.value(), value.offset(), value.length());
}
}
return valid;
}

public boolean validateContent(
HttpRequestType requestType,
DirectBuffer buffer,
int index,
int length)
{
return requestType == null ||
requestType.content == null ||
requestType.content.read(buffer, index, length);
}

private static Function<Function<String, String>, String> orElseIfNull(
Function<Function<String, String>, String> first,
Function<Function<String, String>, String> second)
Expand All @@ -174,4 +386,17 @@ private static Function<Function<String, String>, String> orElseIfNull(
return result != null ? result : second.apply(hs);
};
}

private static String resolveHeaderValue(
HttpBeginExFW beginEx,
String8FW headerName)
{
String result = null;
HttpHeaderFW header = beginEx.headers().matchFirst(h -> headerName.equals(h.name()));
if (header != null)
{
result = header.value().asString();
}
return result;
}
}
Loading