diff --git a/ui-backend/catalog-ui-search/pom.xml b/ui-backend/catalog-ui-search/pom.xml index c4c85cf8d65..e63437fc9ca 100644 --- a/ui-backend/catalog-ui-search/pom.xml +++ b/ui-backend/catalog-ui-search/pom.xml @@ -390,6 +390,11 @@ catalog-rest-api ${ddf.version} + + ddf.catalog.rest + catalog-rest-endpoint + ${ddf.version} + com.google.guava guava @@ -492,7 +497,8 @@ ows-v_1_1_0-schema, jaxb2-basics-runtime, catalog-rest-service, - owasp-java-html-sanitizer + owasp-java-html-sanitizer, + catalog-rest-endpoint ${project.artifactId} diff --git a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/catalog/CatalogApplication.java b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/catalog/CatalogApplication.java index a70bd38719e..52fa1b68b09 100644 --- a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/catalog/CatalogApplication.java +++ b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/catalog/CatalogApplication.java @@ -35,15 +35,23 @@ import javax.servlet.MultipartConfigElement; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.cxf.jaxrs.impl.UriBuilderImpl; import org.apache.http.HttpStatus; +import org.codice.ddf.catalog.ui.config.ConfigurationApplication; +import org.codice.ddf.catalog.ui.util.jaxrs.JaxRsHttpHeaders; +import org.codice.ddf.catalog.ui.util.jaxrs.JaxRsUriInfo; +import org.codice.ddf.catalog.ui.util.multipart.CleanableMultipartBody; +import org.codice.ddf.catalog.ui.util.multipart.CleanableMultipartBodyFactory; +import org.codice.ddf.endpoints.rest.RESTEndpoint; import org.codice.ddf.rest.api.CatalogService; import org.codice.ddf.rest.api.CatalogServiceException; import org.slf4j.Logger; @@ -66,16 +74,27 @@ public class CatalogApplication implements SparkApplication { private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges"; + private static final String HEADER_ID = "id"; + private static final String BYTES = "bytes"; + private static final String CATALOG_PATH = "/catalog/"; + private static final String CATALOG_ID_PATH = "/catalog/:id"; private static final String TRANSFORM = "transform"; + private final ConfigurationApplication config; + private CatalogService catalogService; - public CatalogApplication(CatalogService catalogService) { + private RESTEndpoint restEndpoint; + + public CatalogApplication( + CatalogService catalogService, RESTEndpoint restEndpoint, ConfigurationApplication config) { this.catalogService = catalogService; + this.restEndpoint = restEndpoint; + this.config = config; } @Override @@ -141,35 +160,52 @@ public void init() { }); post( - "/catalog/", + CATALOG_PATH, (req, res) -> { - if (req.contentType().startsWith("multipart/")) { - req.attribute( - ECLIPSE_MULTIPART_CONFIG, - new MultipartConfigElement(System.getProperty(JAVA_IO_TMPDIR))); + try { + LOGGER.debug("POST Path: {}", CATALOG_PATH); + String contentType = req.contentType(); + + // Convert req and res for RESTEndpoint + HttpServletRequest httpRequest = req.raw(); + HttpHeaders headers = new JaxRsHttpHeaders(req); + UriInfo uriInfo = new JaxRsUriInfo(req); + String transformerParam = req.queryParams(TRANSFORM); + InputStream inputStream = httpRequest.getInputStream(); + + if (contentType.startsWith("multipart/")) { + LOGGER.debug("POST Path: {} multipart", CATALOG_PATH); + CleanableMultipartBody multipartBody = + CleanableMultipartBodyFactory.create( + httpRequest, config.getMaximumUploadSize(), config.getMaxFileSizeInMemory()); + + javax.ws.rs.core.Response response = + restEndpoint.addDocument( + headers, uriInfo, httpRequest, multipartBody, transformerParam, inputStream); + + multipartBody.cleanup(); + + return setResponse( + res, httpRequest.getRequestURL(), response.getHeaderString(HEADER_ID)); + } - return addDocument( - res, - req.raw().getRequestURL(), - req.contentType(), - req.queryParams(TRANSFORM), - req.raw(), - new ByteArrayInputStream(req.bodyAsBytes())); - } + if (contentType.startsWith("text/") || contentType.startsWith("application/")) { + LOGGER.debug("POST Path: {} text/application", CATALOG_PATH); + javax.ws.rs.core.Response response = + restEndpoint.addDocument( + headers, uriInfo, httpRequest, transformerParam, inputStream); - if (req.contentType().startsWith("text/") - || req.contentType().startsWith("application/")) { - return addDocument( - res, - req.raw().getRequestURL(), - req.contentType(), - req.queryParams(TRANSFORM), - null, - new ByteArrayInputStream(req.bodyAsBytes())); - } + return setResponse( + res, httpRequest.getRequestURL(), response.getHeaderString(HEADER_ID)); + } - res.status(HttpStatus.SC_NOT_FOUND); - return res; + res.status(HttpStatus.SC_NOT_FOUND); + return "Not Found"; + } catch (Exception e) { + LOGGER.error("Unexpected error in request handler", e); + res.status(HttpStatus.SC_INTERNAL_SERVER_ERROR); + return "Internal Server Error"; + } }); put( @@ -373,19 +409,8 @@ private String updateDocument( } } - private String addDocument( - Response res, - StringBuffer requestUrl, - String contentType, - String transformerParam, - HttpServletRequest httpServletRequest, - InputStream inputStream) { + private String setResponse(Response res, StringBuffer requestUrl, String id) { try { - List contentTypeList = ImmutableList.of(contentType); - String id = - catalogService.addDocument( - contentTypeList, httpServletRequest, transformerParam, inputStream); - URI uri = new URI(requestUrl.toString()); UriBuilder uriBuilder = new UriBuilderImpl(uri).path("/" + id); @@ -394,7 +419,7 @@ private String addDocument( res.header(Metacard.ID, id); return ""; - } catch (CatalogServiceException | URISyntaxException e) { + } catch (URISyntaxException e) { return createBadRequestResponse(res, e.getMessage()); } } diff --git a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/config/ConfigurationApplication.java b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/config/ConfigurationApplication.java index 439b030e8f2..e8884299963 100644 --- a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/config/ConfigurationApplication.java +++ b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/config/ConfigurationApplication.java @@ -168,7 +168,8 @@ public class ConfigurationApplication implements SparkApplication { private String mapHome = ""; - private int maximumUploadSize = 1_048_576; + private long maximumUploadSize = 1_048_576; + private int maxFileSizeInMemory = 50 * 1024 * 1024; // 50 MB private List readOnly = ImmutableList.of( @@ -402,11 +403,11 @@ public void setResultShow(List resultShow) { this.resultShow = resultShow; } - public void setMaximumUploadSize(int size) { + public void setMaximumUploadSize(long size) { this.maximumUploadSize = size; } - public int getMaximumUploadSize() { + public long getMaximumUploadSize() { return maximumUploadSize; } @@ -573,6 +574,8 @@ public Map getConfig() { config.put("menuIconSrc", menuIconSrc); config.put("customBranding", customBranding); config.put("extra", extra); + config.put("maximumUploadSize", maximumUploadSize); + config.put("maxFileSizeInMemory", maxFileSizeInMemory); return config; } @@ -1264,4 +1267,12 @@ public String getMenuIconSrc() { public void setMenuIconSrc(String menuIconSrc) { this.menuIconSrc = menuIconSrc; } + + public void setMaxFileSizeInMemory(int size) { + this.maxFileSizeInMemory = size; + } + + public int getMaxFileSizeInMemory() { + return maxFileSizeInMemory; + } } diff --git a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/jaxrs/JaxRsHttpHeaders.java b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/jaxrs/JaxRsHttpHeaders.java new file mode 100644 index 00000000000..2e7c61e5658 --- /dev/null +++ b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/jaxrs/JaxRsHttpHeaders.java @@ -0,0 +1,80 @@ +package org.codice.ddf.catalog.ui.util.jaxrs; + +import java.util.*; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; + +public class JaxRsHttpHeaders implements HttpHeaders { + private static final Logger LOGGER = LoggerFactory.getLogger(JaxRsHttpHeaders.class); + private final Request sparkRequest; + + public JaxRsHttpHeaders(Request sparkRequest) { + this.sparkRequest = sparkRequest; + } + + @Override + public List getRequestHeader(String name) { + String header = sparkRequest.headers(name); + return header != null ? Collections.singletonList(header) : Collections.emptyList(); + } + + @Override + public String getHeaderString(String s) { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getHeaderString()"); + return ""; + } + + @Override + public MultivaluedMap getRequestHeaders() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getRequestHeaders()"); + return new MultivaluedHashMap<>(); + } + + @Override + public List getAcceptableMediaTypes() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getAcceptableMediaTypes()"); + return List.of(); + } + + @Override + public List getAcceptableLanguages() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getAcceptableLanguages()"); + return List.of(); + } + + @Override + public MediaType getMediaType() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getMediaType()"); + return null; + } + + @Override + public Locale getLanguage() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getLanguage()"); + return null; + } + + @Override + public Map getCookies() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getCookies()"); + return Map.of(); + } + + @Override + public Date getDate() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getDate()"); + return null; + } + + @Override + public int getLength() { + LOGGER.warn("Not implemented: JaxRsHttpHeaders.getLength()"); + return 0; + } +} diff --git a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/jaxrs/JaxRsUriInfo.java b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/jaxrs/JaxRsUriInfo.java new file mode 100644 index 00000000000..a9e5f3b5646 --- /dev/null +++ b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/jaxrs/JaxRsUriInfo.java @@ -0,0 +1,129 @@ +package org.codice.ddf.catalog.ui.util.jaxrs; + +import java.net.URI; +import java.util.*; +import javax.ws.rs.core.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spark.Request; + +public class JaxRsUriInfo implements UriInfo { + + private static final Logger LOGGER = LoggerFactory.getLogger(JaxRsUriInfo.class); + private final Request sparkRequest; + + public JaxRsUriInfo(Request sparkRequest) { + this.sparkRequest = sparkRequest; + } + + @Override + public URI getAbsolutePath() { + return URI.create(sparkRequest.url()); + } + + @Override + public UriBuilder getAbsolutePathBuilder() { + return UriBuilder.fromUri(getAbsolutePath()); + } + + @Override + public URI getRequestUri() { + return URI.create(sparkRequest.url()); + } + + @Override + public String getPath() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getPath()"); + return ""; + } + + @Override + public String getPath(boolean decode) { + LOGGER.warn("Not implemented: JaxRsUriInfo.getPath(boolean)"); + return ""; + } + + @Override + public List getPathSegments() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getPathSegments()"); + return List.of(); + } + + @Override + public List getPathSegments(boolean b) { + LOGGER.warn("Not implemented: JaxRsUriInfo.getPathSegments(boolean)"); + return List.of(); + } + + @Override + public MultivaluedMap getQueryParameters() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getQueryParameters()"); + return new MultivaluedHashMap<>(); + } + + @Override + public MultivaluedMap getQueryParameters(boolean b) { + LOGGER.warn("Not implemented: JaxRsUriInfo.getQueryParameters(boolean)"); + return new MultivaluedHashMap<>(); + } + + @Override + public List getMatchedURIs() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getMatchedURIs()"); + return List.of(); + } + + @Override + public List getMatchedURIs(boolean b) { + LOGGER.warn("Not implemented: JaxRsUriInfo.getMatchedURIs(boolean)"); + return List.of(); + } + + @Override + public List getMatchedResources() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getMatchedResources()"); + return List.of(); + } + + @Override + public URI resolve(URI uri) { + LOGGER.warn("Not implemented: JaxRsUriInfo.resolve()"); + return null; + } + + @Override + public URI relativize(URI uri) { + LOGGER.warn("Not implemented: JaxRsUriInfo.relativize()"); + return null; + } + + @Override + public UriBuilder getRequestUriBuilder() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getRequestUriBuilder()"); + return null; + } + + @Override + public URI getBaseUri() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getBaseUri()"); + return null; + } + + @Override + public UriBuilder getBaseUriBuilder() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getBaseUriBuilder()"); + return null; + } + + @Override + public MultivaluedMap getPathParameters() { + LOGGER.warn("Not implemented: JaxRsUriInfo.getPathParameters()"); + return new MultivaluedHashMap<>(); + } + + @Override + public MultivaluedMap getPathParameters(boolean b) { + LOGGER.warn("Not implemented: JaxRsUriInfo.getPathParameters(boolean)"); + return new MultivaluedHashMap<>(); + } +} diff --git a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/multipart/CleanableMultipartBody.java b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/multipart/CleanableMultipartBody.java new file mode 100644 index 00000000000..b0ec1f3ef0d --- /dev/null +++ b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/multipart/CleanableMultipartBody.java @@ -0,0 +1,33 @@ +package org.codice.ddf.catalog.ui.util.multipart; + +import java.util.Collection; +import java.util.List; +import javax.servlet.http.Part; +import javax.ws.rs.core.MediaType; +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.apache.cxf.jaxrs.ext.multipart.MultipartBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CleanableMultipartBody extends MultipartBody { + + private static final Logger LOGGER = LoggerFactory.getLogger(CleanableMultipartBody.class); + private final Collection parts; + + public CleanableMultipartBody( + List atts, Collection parts, MediaType mt, boolean outbound) { + super(atts, mt, outbound); + this.parts = parts; + } + + public void cleanup() { + try { + for (Part part : parts) { + part.delete(); + LOGGER.debug("Temporary file '{}' deleted successfully", part.getName()); + } + } catch (Exception e) { + LOGGER.error("Failed to delete temporary files", e); + } + } +} diff --git a/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/multipart/CleanableMultipartBodyFactory.java b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/multipart/CleanableMultipartBodyFactory.java new file mode 100644 index 00000000000..7d94e558b02 --- /dev/null +++ b/ui-backend/catalog-ui-search/src/main/java/org/codice/ddf/catalog/ui/util/multipart/CleanableMultipartBodyFactory.java @@ -0,0 +1,81 @@ +package org.codice.ddf.catalog.ui.util.multipart; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; +import javax.ws.rs.core.MediaType; +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CleanableMultipartBodyFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(CleanableMultipartBodyFactory.class); + + private static final String ECLIPSE_MULTIPART_CONFIG = "org.eclipse.jetty.multipartConfig"; + private static final String JAVA_IO_TMPDIR = "java.io.tmpdir"; + + private CleanableMultipartBodyFactory() {} + + /** + * Creates a Cleanable MultipartBody object from HttpServletRequest + * + * @param httpRequest the request object + * @param maxUploadSize the maximum allowed uploaded file size + * @param fileSizeThreshold the file size threshold stored in memory before written to disk + * @return a Cleanable MultipartBody + * @throws ServletException + * @throws IOException + */ + public static CleanableMultipartBody create( + HttpServletRequest httpRequest, long maxUploadSize, int fileSizeThreshold) + throws ServletException, IOException { + String location = System.getProperty(JAVA_IO_TMPDIR); + MultipartConfigElement multipartConfigElement = + new MultipartConfigElement(location, maxUploadSize, maxUploadSize, fileSizeThreshold); + + LOGGER.debug( + "Multipart Config Element: location={}, maxFileSize={}, maxRequestSize={}, fileSizeThreshold={}", + location, + maxUploadSize, + maxUploadSize, + fileSizeThreshold); + + httpRequest.setAttribute(ECLIPSE_MULTIPART_CONFIG, multipartConfigElement); + List attachments = new ArrayList<>(); + Collection parts; + + parts = httpRequest.getParts(); + + for (Part part : parts) { + String name = part.getName(); + String fileName = part.getSubmittedFileName(); + long size = part.getSize(); + String contentType = part.getContentType(); + + LOGGER.debug( + "Processing part: name={}, fileName={}, size={}, contentType={}", + name, + fileName, + size, + contentType); + + if (size > 0) { + InputStream partStream = part.getInputStream(); + ContentDisposition contentDisposition = + new ContentDisposition( + "form-data; name=\"" + name + "\"; filename=\"" + fileName + "\""); + + attachments.add(new Attachment(name, partStream, contentDisposition)); + } else { + LOGGER.warn("Ignored part with empty content: name={}, fileName={}", name, fileName); + } + } + + return new CleanableMultipartBody(attachments, parts, MediaType.MULTIPART_FORM_DATA_TYPE, true); + } +} diff --git a/ui-backend/catalog-ui-search/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/ui-backend/catalog-ui-search/src/main/resources/OSGI-INF/blueprint/blueprint.xml index 390132c9228..94d6100651d 100644 --- a/ui-backend/catalog-ui-search/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/ui-backend/catalog-ui-search/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -147,6 +147,12 @@ Implementation details + + + + + +