Skip to content

Commit

Permalink
[GEOS-11148] Update response headers for the Resources REST API (#7163)
Browse files Browse the repository at this point in the history
Co-authored-by: Steve Ikeoka <steve.ikeoka@gdit.com>
  • Loading branch information
aaime and sikeoka committed Oct 9, 2023
1 parent ffce0fc commit 7db9857
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import freemarker.template.ObjectWrapper;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
Expand All @@ -23,6 +20,7 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -53,6 +51,7 @@
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
Expand Down Expand Up @@ -129,24 +128,18 @@ protected String getTemplateName(Object object) {
* @return Content type requested
*/
protected static MediaType getMediaType(Resource resource, HttpServletRequest request) {
if (resource.getType() == Resource.Type.DIRECTORY) {
return getFormat(request);
} else if (resource.getType() == Resource.Type.RESOURCE) {
String mimeType = URLConnection.guessContentTypeFromName(resource.name());
if (mimeType == null
|| MediaType.APPLICATION_OCTET_STREAM.toString().equals(mimeType)) {
// try guessing from data
try (InputStream is = new BufferedInputStream(resource.in())) {
mimeType = URLConnection.guessContentTypeFromStream(is);
} catch (IOException e) {
// do nothing, we'll just use application/octet-stream
}
}
return mimeType == null
? MediaType.APPLICATION_OCTET_STREAM
: MediaType.valueOf(mimeType);
} else {
return null;
switch (resource.getType()) {
case DIRECTORY:
return getFormat(request);
case RESOURCE:
// set the mime if known by the servlet container, otherwise default to
// application/octet-stream to mitigate potential cross-site scripting
return Optional.ofNullable(request.getServletContext())
.map(sc -> sc.getMimeType(resource.name()))
.map(MediaType::valueOf)
.orElse(MediaType.APPLICATION_OCTET_STREAM);
default:
throw new ResourceNotFoundException("Undefined resource path.");
}
}

Expand Down Expand Up @@ -265,21 +258,27 @@ public Object resourceGet(
Resource resource = resource(request);
Operation operation = operation(operationName);
Object result;
response.setContentType(getFormat(format).toString());

if (operation == Operation.METADATA) {
result =
wrapObject(
new ResourceMetadataInfo(resource, request),
ResourceMetadataInfo.class);
response.setContentType(getFormat(format).toString());
} else {
if (resource.getType() == Resource.Type.UNDEFINED) {
throw new ResourceNotFoundException("Undefined resource path.");
} else {
HttpHeaders responseHeaders = new HttpHeaders();
MediaType mediaType = getMediaType(resource, request);
responseHeaders.setContentType(mediaType);
response.setContentType(mediaType.toString());
if (resource.getType() == Resource.Type.RESOURCE) {
// Use Content-Disposition: attachment to mitigate potential XSS issues
responseHeaders.setContentDisposition(
ContentDisposition.builder("attachment")
.filename(resource.name())
.build());
}

if (request.getMethod().equals("HEAD")) {
result = new ResponseEntity<>("", responseHeaders, HttpStatus.OK);
Expand All @@ -288,6 +287,7 @@ public Object resourceGet(
wrapObject(
new ResourceDirectoryInfo(resource, request),
ResourceDirectoryInfo.class);
response.setContentType(mediaType.toString());
} else {
result = new ResponseEntity<>(resource.in(), responseHeaders, HttpStatus.OK);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
package org.geoserver.rest.resources;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.DateFormat;
Expand Down Expand Up @@ -239,6 +239,11 @@ public void testResourceMetadataHTML() throws Exception {
public void testResourceHeaders() throws Exception {
MockHttpServletResponse response =
getAsServletResponse(RestBaseController.ROOT_PATH + "/resource/mydir2/fake.png");
assertEquals(
"http://localhost:8080/geoserver"
+ RestBaseController.ROOT_PATH
+ "/resource/mydir2/fake.png",
response.getHeader("Location"));
Assert.assertEquals(
FORMAT_HEADER.format(getDataDirectory().get("mydir2/fake.png").lastmodified()),
response.getHeader("Last-Modified"));
Expand All @@ -249,12 +254,19 @@ public void testResourceHeaders() throws Exception {
response.getHeader("Resource-Parent"));
Assert.assertEquals("resource", response.getHeader("Resource-Type"));
assertContentType("image/png", response);
assertEquals(
"attachment; filename=\"fake.png\"", response.getHeader("Content-Disposition"));
}

@Test
public void testResourceHead() throws Exception {
MockHttpServletResponse response =
headAsServletResponse(RestBaseController.ROOT_PATH + "/resource/mydir2/fake.png");
assertEquals(
"http://localhost:8080/geoserver"
+ RestBaseController.ROOT_PATH
+ "/resource/mydir2/fake.png",
response.getHeader("Location"));
Assert.assertEquals(
FORMAT_HEADER.format(getDataDirectory().get("mydir2/fake.png").lastmodified()),
response.getHeader("Last-Modified"));
Expand All @@ -265,6 +277,8 @@ public void testResourceHead() throws Exception {
response.getHeader("Resource-Parent"));
Assert.assertEquals("resource", response.getHeader("Resource-Type"));
assertContentType("image/png", response);
assertEquals(
"attachment; filename=\"fake.png\"", response.getHeader("Content-Disposition"));
}

@Test
Expand Down Expand Up @@ -409,15 +423,15 @@ public void testDirectoryJSONMultipleChildren() throws Exception {
+ " 'link': {\n"
+ " 'href': 'http://localhost:8080/geoserver/rest/resource/mydir2/imagewithoutextension',\n"
+ " 'rel': 'alternate',\n"
+ " 'type': 'image/png'\n"
+ " 'type': 'application/octet-stream'\n"
+ " }\n"
+ " },\n"
+ " {\n"
+ " 'name': 'myres.json',\n"
+ " 'link': {\n"
+ " 'href': 'http://localhost:8080/geoserver/rest/resource/mydir2/myres.json',\n"
+ " 'rel': 'alternate',\n"
+ " 'type': 'application/octet-stream'\n"
+ " 'type': 'application/json'\n"
+ " }\n"
+ " },\n"
+ " {\n"
Expand All @@ -430,9 +444,6 @@ public void testDirectoryJSONMultipleChildren() throws Exception {
+ " }\n"
+ " ]}\n"
+ "}}";
// starting with JDK 17 (v3?) json is correctly recognized, the test output
String jsonType = URLConnection.guessContentTypeFromName("test.json");
if (jsonType != null) expected = expected.replace("application/octet-stream", jsonType);
JSONAssert.assertEquals(expected, (JSONObject) json);
}

Expand Down Expand Up @@ -508,7 +519,7 @@ public void testDirectoryMimeTypes() throws Exception {
Document doc = getAsDOM(RestBaseController.ROOT_PATH + "/resource/mydir2?format=xml");
// print(doc);
XMLAssert.assertXpathEvaluatesTo(
"image/png",
"application/octet-stream",
"/ResourceDirectory/children/child[name='imagewithoutextension']/atom:link/@type",
doc);
XMLAssert.assertXpathEvaluatesTo(
Expand Down

0 comments on commit 7db9857

Please sign in to comment.