-
Notifications
You must be signed in to change notification settings - Fork 25k
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
Validate Accept and Content-Type header for compatible API #54103
Changes from 55 commits
a4f80ca
c99b818
c9c0e55
258542e
548fbd9
0dbea8a
4cf6bc1
3ac22b1
38721fe
f2db19f
b83b4ce
cf1d340
5c4a02d
0ef7e18
835ce56
f440231
84f1dde
d106d1b
cf61bbd
f00f438
ae1799f
a0fce89
7e55744
00fe62d
a6f0b9a
4eff534
2d4161c
6830f97
587f84d
7b092f5
ef25920
1c2fece
2374c20
eeae41f
e7d9b29
268bc1c
15f4be5
017c804
3d975f3
09ef281
bf09243
30f1c40
a013a03
6c28434
37d6426
1d57fe7
0662ef3
86f8bc0
b292080
a4f472d
bb827bc
e9a4fbd
f876d46
47fd5cb
bcb11fd
225a00b
ebaa7c9
c96bd86
10e8d1b
3181daf
9bd396a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,6 +80,7 @@ public class Version implements Comparable<Version>, ToXContentFragment { | |
public static final Version V_7_8_0 = new Version(7080099, org.apache.lucene.util.Version.LUCENE_8_5_0); | ||
public static final Version V_8_0_0 = new Version(8000099, org.apache.lucene.util.Version.LUCENE_8_5_0); | ||
public static final Version CURRENT = V_8_0_0; | ||
public static final Version PREVIOUS = V_7_0_0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit ambiguous, and probably not a necessary as a static constant. Can we add in a method like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In doing so, can you please make the method static? It should only apply to the CURRENT version. The fact the other compatibility methods are on Version instances makes testing and modifying the rules difficult, even though in practice the CURRENT version's compatibility is all we need, and the others are just artificial for testing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree, the method should be static |
||
|
||
private static final ImmutableOpenIntMap<Version> idToVersion; | ||
|
||
|
@@ -89,7 +90,7 @@ public class Version implements Comparable<Version>, ToXContentFragment { | |
for (final Field declaredField : Version.class.getFields()) { | ||
if (declaredField.getType().equals(Version.class)) { | ||
final String fieldName = declaredField.getName(); | ||
if (fieldName.equals("CURRENT") || fieldName.equals("V_EMPTY")) { | ||
if (fieldName.equals("CURRENT") || fieldName.equals("V_EMPTY") || fieldName.equals("PREVIOUS")) { | ||
continue; | ||
} | ||
assert fieldName.matches("V_\\d+_\\d+_\\d+") | ||
|
@@ -402,6 +403,7 @@ public static List<Version> getDeclaredVersions(final Class<?> versionClass) { | |
} | ||
switch (field.getName()) { | ||
case "CURRENT": | ||
case "PREVIOUS": | ||
case "V_EMPTY": | ||
continue; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -352,6 +352,11 @@ private void handleIncomingRequest(final HttpRequest httpRequest, final HttpChan | |
} catch (final RestRequest.BadParameterException e) { | ||
badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e); | ||
innerRestRequest = RestRequest.requestWithoutParameters(xContentRegistry, httpRequest, httpChannel); | ||
} catch (final RestRequest.CompatibleApiHeadersCombinationException e){ | ||
badRequestCause = ExceptionsHelper.useOrSuppress(badRequestCause, e); | ||
//todo // tempting to just rethrow. removing content type is just making this less obvious | ||
throw e; | ||
// innerRestRequest = requestWithoutContentTypeHeader(httpRequest, httpChannel, badRequestCause); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you clean up the this todo abit ? Not sure I follow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just throwing an exception was helping checking the logs, but indeed was breaking the channel creation and there was no response sent back.. |
||
} | ||
restRequest = innerRestRequest; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
|
||
import org.apache.lucene.util.SetOnce; | ||
import org.elasticsearch.ElasticsearchParseException; | ||
import org.elasticsearch.Version; | ||
import org.elasticsearch.common.Booleans; | ||
import org.elasticsearch.common.CheckedConsumer; | ||
import org.elasticsearch.common.Nullable; | ||
|
@@ -45,6 +46,7 @@ | |
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
|
@@ -132,29 +134,91 @@ void ensureSafeBuffers() { | |
public static RestRequest request(NamedXContentRegistry xContentRegistry, HttpRequest httpRequest, HttpChannel httpChannel) { | ||
Map<String, String> params = params(httpRequest.uri()); | ||
String path = path(httpRequest.uri()); | ||
RestRequest restRequest = new RestRequest(xContentRegistry, params, path, httpRequest.getHeaders(), httpRequest, httpChannel, | ||
return new RestRequest(xContentRegistry, params, path, httpRequest.getHeaders(), httpRequest, httpChannel, | ||
requestIdGenerator.incrementAndGet()); | ||
return restRequest; | ||
} | ||
|
||
private void addCompatibleParameter() { | ||
if (isRequestCompatible()) { | ||
String compatibleVersion = XContentType.parseVersion(header(CompatibleConstants.COMPATIBLE_HEADER)); | ||
params().put(CompatibleConstants.COMPATIBLE_PARAMS_KEY, compatibleVersion); | ||
Version compatible = compatibleWithVersion(); | ||
if (Version.PREVIOUS.equals(compatible)) { | ||
params().put(CompatibleConstants.COMPATIBLE_PARAMS_KEY, String.valueOf(compatible.major)); | ||
//use it so it won't fail request validation with unused parameter | ||
param(CompatibleConstants.COMPATIBLE_PARAMS_KEY); | ||
} | ||
} | ||
|
||
private boolean isRequestCompatible() { | ||
return isHeaderCompatible(header(CompatibleConstants.COMPATIBLE_HEADER)); | ||
} | ||
private Version compatibleWithVersion() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be greatly simplified and read easier if this returns a boolean and only concerns itself with the version (not the supported media types (that can happen later if need be)). I think something like this below the same level of validation.
|
||
String currentVersion = String.valueOf(Version.CURRENT.major); | ||
String previousVersion = String.valueOf(Version.CURRENT.major - 1); | ||
|
||
private boolean isHeaderCompatible(String headerValue) { | ||
String version = XContentType.parseVersion(headerValue); | ||
return CompatibleConstants.COMPATIBLE_VERSION.equals(version); | ||
} | ||
String acceptHeader = header(CompatibleConstants.COMPATIBLE_ACCEPT_HEADER); | ||
String acceptVersion = XContentType.parseVersion(acceptHeader); | ||
String contentTypeHeader = header(CompatibleConstants.COMPATIBLE_CONTENT_TYPE_HEADER); | ||
String contentTypeVersion = XContentType.parseVersion(contentTypeHeader); | ||
|
||
//TODO not sure about this one as this does not cover text formats | ||
boolean isSupportedMediaTypeAccept = acceptHeader == null || XContentType.parseMediaType(acceptHeader) != null; | ||
|
||
boolean isSupportedMediaTypeContentType = contentTypeHeader == null || XContentType.parseMediaType(contentTypeHeader) != null; | ||
|
||
if (hasContent()) { | ||
//both headers versioned | ||
if (acceptVersion != null && contentTypeVersion != null) { | ||
// both Accept and Content-Type are versioned and set to a previous version | ||
if (previousVersion.equals(acceptVersion) && previousVersion.equals(contentTypeVersion)) { | ||
return Version.PREVIOUS; | ||
} | ||
// both Accept and Content-Type are versioned to a current version | ||
if (currentVersion.equals(acceptVersion) && currentVersion.equals(contentTypeVersion)) { | ||
return Version.CURRENT; | ||
} | ||
// both headers are versioned but set to incorrect version | ||
throw new CompatibleApiHeadersCombinationException( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Throwing the exception here results in an abrupt termination of the connection (no error code or proper response). Not sure the fix, but we should add a REST test to ensure the error code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we allow RestRequest to be in an incorrect state. When creating a request and it fails due to This makes it harder to create a validation of headers.. we would need to allow to have an |
||
String.format(Locale.ROOT, "Request with a body and incompatible Accept and Content-Type header values. " + | ||
"Accept=%s Content-Type=%s hasContent=%b %s %s %s", acceptHeader, | ||
contentTypeHeader, hasContent(), path(), params.toString(), method().toString())); | ||
} | ||
// Content-Type is versioned but accept is not present | ||
if(previousVersion.equals(contentTypeVersion) | ||
&& (acceptHeader == null || acceptHeader.equals("*/*") )) { | ||
return Version.PREVIOUS; | ||
} | ||
// Content type is set (not verioned) but accept is not present. It will be defaulted to JSON | ||
if(isSupportedMediaTypeContentType && | ||
(acceptHeader == null || acceptHeader.equals("*/*") )){//TODO when do we default this? | ||
return Version.CURRENT; | ||
} | ||
// both Accept and Content-Type are not a compatible format, but are supported and not empty | ||
if (isSupportedMediaTypeContentType && isSupportedMediaTypeAccept && | ||
acceptHeader != null && contentTypeHeader != null) { | ||
return Version.CURRENT; | ||
} | ||
} else { | ||
if (acceptVersion != null) { | ||
//Accept header is versioned and set to previous | ||
if (previousVersion.equals(acceptVersion)) { | ||
return Version.PREVIOUS; | ||
} | ||
if (currentVersion.equals(acceptVersion)) { | ||
return Version.CURRENT; | ||
} | ||
// Accept header is versioned but set to incorrect version | ||
throw new CompatibleApiHeadersCombinationException( | ||
String.format(Locale.ROOT, "Request with a body and incompatible Accept and Content-Type header values. " + | ||
"Accept=%s Content-Type=%s hasContent=%b %s %s %s", acceptHeader, | ||
contentTypeHeader, hasContent(), path(), params.toString(), method().toString())); | ||
} | ||
//Accept header is not versioned but is supported | ||
if (isSupportedMediaTypeAccept) { | ||
return Version.CURRENT; | ||
} | ||
} | ||
|
||
throw new CompatibleApiHeadersCombinationException( | ||
String.format(Locale.ROOT, "Request with a body and incompatible Accept and Content-Type header values. " + | ||
"Accept=%s Content-Type=%s hasContent=%b %s %s %s", acceptHeader, | ||
contentTypeHeader, hasContent(), path(), params.toString(), method().toString())); | ||
} | ||
|
||
private static Map<String, String> params(final String uri) { | ||
final Map<String, String> params = new HashMap<>(); | ||
|
@@ -559,4 +623,12 @@ public static class BadParameterException extends RuntimeException { | |
|
||
} | ||
|
||
public static class CompatibleApiHeadersCombinationException extends RuntimeException { | ||
|
||
CompatibleApiHeadersCombinationException(String cause) { | ||
super(cause); | ||
} | ||
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,4 +121,21 @@ public void testVersionParsing() { | |
assertThat(XContentType.parseVersion("APPLICATION/JSON"), | ||
nullValue()); | ||
} | ||
|
||
public void testVersionParsingOnText() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as per my comment https://github.com/elastic/elasticsearch/pull/54103/files#r409392721 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we postpone that decision for now and work and in future work ensure that text based APIs are good ? |
||
String version = String.valueOf(Math.abs(randomByte())); | ||
assertThat(XContentType.parseVersion("text/vnd.elasticsearch+csv;compatible-with=" + version), | ||
equalTo(version)); | ||
assertThat(XContentType.parseVersion("text/vnd.elasticsearch+text;compatible-with=" + version), | ||
equalTo(version)); | ||
assertThat(XContentType.parseVersion("text/vnd.elasticsearch+tab-separated-values;compatible-with=" + version), | ||
equalTo(version)); | ||
assertThat(XContentType.parseVersion("text/csv"), | ||
nullValue()); | ||
|
||
assertThat(XContentType.parseVersion("TEXT/VND.ELASTICSEARCH+CSV;COMPATIBLE-WITH=" + version), | ||
equalTo(version)); | ||
assertThat(XContentType.parseVersion("TEXT/csv"), | ||
nullValue()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have tests that need Accept header to be empty - expecting text response
maybe we should extend XContentType to
text
but then again, we don't expect content-type to be text, and adding text there would allow this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can punt on the cat api's for now, and this is sufficient for now. this code is temporary and i can address this later.