Properly encode location header

Today when trying to encode the location header to ASCII, we rely on the
Java URI API. This API requires a proper URI which blows up whenever the
URI contains, for example, a space (which can happen if the type, ID, or
routing contain a space). This commit addresses this issue by properly
encoding the URI. Additionally, we remove the need to create a URI
simplifying the code flow.

Relates #23133
This commit is contained in:
Jason Tedor 2017-02-13 09:34:52 -05:00 committed by GitHub
parent 072748cd67
commit 9dff5e2af7
5 changed files with 69 additions and 75 deletions

View File

@ -38,8 +38,11 @@ import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.RestStatus;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Locale; import java.util.Locale;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
@ -201,31 +204,43 @@ public abstract class DocWriteResponse extends ReplicationResponse implements Wr
} }
/** /**
* Gets the location of the written document as a string suitable for a {@code Location} header. * Return the relative URI for the location of the document suitable for use in the {@code Location} header. The use of relative URIs is
* @param routing any routing used in the request. If null the location doesn't include routing information. * permitted as of HTTP/1.1 (cf. https://tools.ietf.org/html/rfc7231#section-7.1.2).
* *
* @param routing custom routing or {@code null} if custom routing is not used
* @return the relative URI for the location of the document
*/ */
public String getLocation(@Nullable String routing) throws URISyntaxException { public String getLocation(@Nullable String routing) {
// Absolute path for the location of the document. This should be allowed as of HTTP/1.1: final String encodedIndex;
// https://tools.ietf.org/html/rfc7231#section-7.1.2 final String encodedType;
String index = getIndex(); final String encodedId;
String type = getType(); final String encodedRouting;
String id = getId(); try {
String routingStart = "?routing="; // encode the path components separately otherwise the path separators will be encoded
int bufferSize = 3 + index.length() + type.length() + id.length(); encodedIndex = URLEncoder.encode(getIndex(), "UTF-8");
if (routing != null) { encodedType = URLEncoder.encode(getType(), "UTF-8");
bufferSize += routingStart.length() + routing.length(); encodedId = URLEncoder.encode(getId(), "UTF-8");
encodedRouting = routing == null ? null : URLEncoder.encode(routing, "UTF-8");
} catch (final UnsupportedEncodingException e) {
throw new AssertionError(e);
} }
StringBuilder location = new StringBuilder(bufferSize); final String routingStart = "?routing=";
location.append('/').append(index); final int bufferSizeExcludingRouting = 3 + encodedIndex.length() + encodedType.length() + encodedId.length();
location.append('/').append(type); final int bufferSize;
location.append('/').append(id); if (encodedRouting == null) {
if (routing != null) { bufferSize = bufferSizeExcludingRouting;
location.append(routingStart).append(routing); } else {
bufferSize = bufferSizeExcludingRouting + routingStart.length() + encodedRouting.length();
}
final StringBuilder location = new StringBuilder(bufferSize);
location.append('/').append(encodedIndex);
location.append('/').append(encodedType);
location.append('/').append(encodedId);
if (encodedRouting != null) {
location.append(routingStart).append(encodedRouting);
} }
URI uri = new URI(location.toString()); return location.toString();
return uri.toASCIIString();
} }
@Override @Override

View File

@ -57,7 +57,7 @@ public class RestStatusToXContentListener<Response extends StatusToXContentObjec
response.toXContent(builder, channel.request()); response.toXContent(builder, channel.request());
RestResponse restResponse = new BytesRestResponse(response.status(), builder); RestResponse restResponse = new BytesRestResponse(response.status(), builder);
if (RestStatus.CREATED == restResponse.status()) { if (RestStatus.CREATED == restResponse.status()) {
String location = extractLocation.apply(response); final String location = extractLocation.apply(response);
if (location != null) { if (location != null) {
restResponse.addHeader("Location", location); restResponse.addHeader("Location", location);
} }

View File

@ -31,7 +31,6 @@ import org.elasticsearch.rest.action.RestActions;
import org.elasticsearch.rest.action.RestStatusToXContentListener; import org.elasticsearch.rest.action.RestStatusToXContentListener;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT; import static org.elasticsearch.rest.RestRequest.Method.PUT;
@ -80,14 +79,7 @@ public class RestIndexAction extends BaseRestHandler {
} }
return channel -> return channel ->
client.index(indexRequest, new RestStatusToXContentListener<>(channel, r -> { client.index(indexRequest, new RestStatusToXContentListener<>(channel, r -> r.getLocation(indexRequest.routing())));
try {
return r.getLocation(indexRequest.routing());
} catch (URISyntaxException ex) {
logger.warn("Location string is not a valid URI.", ex);
return null;
}
}));
} }
} }

View File

@ -36,7 +36,6 @@ import org.elasticsearch.rest.action.RestStatusToXContentListener;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import static org.elasticsearch.rest.RestRequest.Method.POST; import static org.elasticsearch.rest.RestRequest.Method.POST;
@ -98,13 +97,7 @@ public class RestUpdateAction extends BaseRestHandler {
}); });
return channel -> return channel ->
client.update(updateRequest, new RestStatusToXContentListener<>(channel, r -> { client.update(updateRequest, new RestStatusToXContentListener<>(channel, r -> r.getLocation(updateRequest.routing())));
try {
return r.getLocation(updateRequest.routing());
} catch (URISyntaxException ex) {
logger.warn("Location string is not a valid URI.", ex);
return null;
}
}));
} }
} }

View File

@ -30,55 +30,49 @@ import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
public class DocWriteResponseTests extends ESTestCase { public class DocWriteResponseTests extends ESTestCase {
public void testGetLocation() throws URISyntaxException { public void testGetLocation() {
DocWriteResponse response = final DocWriteResponse response =
new DocWriteResponse( new DocWriteResponse(
new ShardId("index", "uuid", 0), new ShardId("index", "uuid", 0),
"type", "type",
"id", "id",
SequenceNumbersService.UNASSIGNED_SEQ_NO, SequenceNumbersService.UNASSIGNED_SEQ_NO,
0, 0,
Result.CREATED) { Result.CREATED) {};
// DocWriteResponse is abstract so we have to sneak a subclass in here to test it.
};
assertEquals("/index/type/id", response.getLocation(null)); assertEquals("/index/type/id", response.getLocation(null));
assertEquals("/index/type/id?routing=test_routing", response.getLocation("test_routing")); assertEquals("/index/type/id?routing=test_routing", response.getLocation("test_routing"));
} }
public void testGetLocationNonAscii() throws URISyntaxException { public void testGetLocationNonAscii() {
DocWriteResponse response = final DocWriteResponse response =
new DocWriteResponse( new DocWriteResponse(
new ShardId("index", "uuid", 0), new ShardId("index", "uuid", 0),
"type", "type",
"", "",
SequenceNumbersService.UNASSIGNED_SEQ_NO, SequenceNumbersService.UNASSIGNED_SEQ_NO,
0, 0,
Result.CREATED) { Result.CREATED) {};
};
assertEquals("/index/type/%E2%9D%A4", response.getLocation(null)); assertEquals("/index/type/%E2%9D%A4", response.getLocation(null));
assertEquals("/index/type/%E2%9D%A4?routing=%C3%A4", response.getLocation("%C3%A4")); assertEquals("/index/type/%E2%9D%A4?routing=%C3%A4", response.getLocation("ä"));
} }
public void testInvalidGetLocation() { public void testGetLocationWithSpaces() {
String invalidPath = "!^*$(@!^!#@"; final DocWriteResponse response =
DocWriteResponse invalid = new DocWriteResponse(
new DocWriteResponse( new ShardId("index", "uuid", 0),
new ShardId("index", "uuid", 0), "type",
"type", "a b",
invalidPath, SequenceNumbersService.UNASSIGNED_SEQ_NO,
SequenceNumbersService.UNASSIGNED_SEQ_NO, 0,
0, Result.CREATED) {};
Result.CREATED) { assertEquals("/index/type/a+b", response.getLocation(null));
}; assertEquals("/index/type/a+b?routing=c+d", response.getLocation("c d"));
Throwable exception = expectThrows(URISyntaxException.class, () -> invalid.getLocation(null));
assertTrue(exception.getMessage().contains(invalidPath));
} }
/** /**