From cd9e3e52ed5f789397e697b0b3cbc7bd634e0e66 Mon Sep 17 00:00:00 2001 From: Madawa Soysa Date: Fri, 21 Jun 2019 15:13:08 +1000 Subject: [PATCH] Implement multipart request handling through invoker This commit improves the Invoker handler to handle multi part requests by generating the proxy request body using the body of the incoming multi part requests. Fixes entgra/product-iots#111 Related to entgra/product-iots#103 --- .../ApplicationManagementPublisherAPI.java | 12 +- ...ApplicationManagementPublisherAPIImpl.java | 12 +- .../io.entgra.ui.request.interceptor/pom.xml | 5 + .../request/interceptor/InvokerHandler.java | 119 +++++++++++++----- .../interceptor/util/HandlerConstants.java | 3 +- pom.xml | 6 + 6 files changed, 112 insertions(+), 45 deletions(-) diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementPublisherAPI.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementPublisherAPI.java index 99355e5c960..699a97f78ac 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementPublisherAPI.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementPublisherAPI.java @@ -273,7 +273,7 @@ public interface ApplicationManagementPublisherAPI { @POST @Path("/ent-app") @Produces(MediaType.APPLICATION_JSON) - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @ApiOperation( consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON, @@ -342,7 +342,7 @@ public interface ApplicationManagementPublisherAPI { @POST @Path("/web-app") @Produces(MediaType.APPLICATION_JSON) - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @ApiOperation( consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON, @@ -406,7 +406,7 @@ public interface ApplicationManagementPublisherAPI { @POST @Path("/public-app") @Produces(MediaType.APPLICATION_JSON) - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @ApiOperation( consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON, @@ -467,7 +467,7 @@ public interface ApplicationManagementPublisherAPI { @POST @Produces(MediaType.APPLICATION_JSON) - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Path("/ent-app/{appId}") @ApiOperation( consumes = MediaType.APPLICATION_JSON, @@ -583,7 +583,7 @@ public interface ApplicationManagementPublisherAPI { @PUT @Path("/image-artifacts/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @ApiOperation( consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON, @@ -653,7 +653,7 @@ public interface ApplicationManagementPublisherAPI { @PUT @Path("/app-artifacts/{deviceType}/{appType}/{appId}/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @ApiOperation( consumes = MediaType.MULTIPART_FORM_DATA, produces = MediaType.APPLICATION_JSON, diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementPublisherAPIImpl.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementPublisherAPIImpl.java index d0d091df032..f3669c09790 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementPublisherAPIImpl.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementPublisherAPIImpl.java @@ -164,7 +164,7 @@ public class ApplicationManagementPublisherAPIImpl implements ApplicationManagem } @POST - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Path("/ent-app") public Response createEntApp( @Multipart("application") ApplicationWrapper applicationWrapper, @@ -204,7 +204,7 @@ public class ApplicationManagementPublisherAPIImpl implements ApplicationManagem } @POST - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Path("/web-app") public Response createWebApp( @Multipart("webapp") WebAppWrapper webAppWrapper, @@ -242,7 +242,7 @@ public class ApplicationManagementPublisherAPIImpl implements ApplicationManagem } @POST - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Path("/public-app") public Response createPubApp( @Multipart("public-app") PublicAppWrapper publicAppWrapper, @@ -280,7 +280,7 @@ public class ApplicationManagementPublisherAPIImpl implements ApplicationManagem } @POST - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Path("/ent-app/{appId}") public Response createEntAppRelease( @PathParam("appId") int appId, @@ -320,7 +320,7 @@ public class ApplicationManagementPublisherAPIImpl implements ApplicationManagem @Override @PUT - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Produces(MediaType.APPLICATION_JSON) @Path("/image-artifacts/{uuid}") public Response updateApplicationImageArtifacts( @@ -357,7 +357,7 @@ public class ApplicationManagementPublisherAPIImpl implements ApplicationManagem @Override @PUT - @Consumes("multipart/mixed") + @Consumes({"multipart/mixed", MediaType.MULTIPART_FORM_DATA}) @Path("/app-artifact/{deviceType}/{appType}/{uuid}") public Response updateApplicationArtifact( @PathParam("deviceType") String deviceType, diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/pom.xml b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/pom.xml index 2fe4013823a..d2cc394427a 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/pom.xml +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/pom.xml @@ -151,5 +151,10 @@ org.wso2.carbon.device.application.mgt.common provided + + org.apache.httpcomponents + httpmime + compile + \ No newline at end of file diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/InvokerHandler.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/InvokerHandler.java index 124f1e5c4c4..864f5d68014 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/InvokerHandler.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/InvokerHandler.java @@ -24,12 +24,17 @@ import com.google.gson.JsonParser; import io.entgra.ui.request.interceptor.beans.AuthData; import io.entgra.ui.request.interceptor.util.HandlerConstants; import io.entgra.ui.request.interceptor.util.HandlerUtil; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; @@ -38,6 +43,9 @@ import org.apache.http.cookie.SM; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.InputStreamBody; import org.wso2.carbon.device.application.mgt.common.ProxyResponse; import javax.servlet.annotation.MultipartConfig; @@ -48,6 +56,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.Enumeration; +import java.util.List; import static io.entgra.ui.request.interceptor.util.HandlerUtil.execute; @@ -61,25 +70,19 @@ import static io.entgra.ui.request.interceptor.util.HandlerUtil.execute; } ) public class InvokerHandler extends HttpServlet { - private static final Log log = LogFactory.getLog(LoginHandler.class); + private static final Log log = LogFactory.getLog(InvokerHandler.class); private static final long serialVersionUID = -6508020875358160165L; - private static AuthData authData; - private static String apiEndpoint; - private static String serverUrl; - private static String platform; + private AuthData authData; + private String apiEndpoint; + private String serverUrl; + private String platform; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { try { if (validateRequest(req, resp)) { HttpPost postRequest = new HttpPost(generateBackendRequestURL(req)); - if (StringUtils.isNotEmpty(req.getHeader(HttpHeaders.CONTENT_LENGTH)) || - StringUtils.isNotEmpty(req.getHeader(HttpHeaders.TRANSFER_ENCODING))) { - InputStreamEntity entity = new InputStreamEntity(req.getInputStream(), - Long.parseLong(req.getHeader(HttpHeaders.CONTENT_LENGTH))); - postRequest.setEntity(entity); - } - copyRequestHeaders(req, postRequest); + generateRequestEntity(req, postRequest); postRequest.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BEARER + authData.getAccessToken()); ProxyResponse proxyResponse = execute(postRequest); @@ -96,6 +99,8 @@ public class InvokerHandler extends HttpServlet { } HandlerUtil.handleSuccess(req, resp, serverUrl, platform, proxyResponse); } + } catch (FileUploadException e) { + log.error("Error occurred when processing Multipart POST request.", e); } catch (IOException e) { log.error("Error occurred when processing POST request.", e); } @@ -106,7 +111,7 @@ public class InvokerHandler extends HttpServlet { try { if (validateRequest(req, resp)) { HttpGet getRequest = new HttpGet(generateBackendRequestURL(req)); - copyRequestHeaders(req, getRequest); + copyRequestHeaders(req, getRequest, false); getRequest.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BEARER + authData.getAccessToken()); ProxyResponse proxyResponse = execute(getRequest); if (HandlerConstants.TOKEN_IS_EXPIRED.equals(proxyResponse.getExecutorResponse())) { @@ -132,14 +137,7 @@ public class InvokerHandler extends HttpServlet { try { if (validateRequest(req, resp)) { HttpPut putRequest = new HttpPut(generateBackendRequestURL(req)); - if ((StringUtils.isNotEmpty(req.getHeader(HttpHeaders.CONTENT_LENGTH)) && - Double.parseDouble(req.getHeader(HttpHeaders.CONTENT_LENGTH)) > 0) || - StringUtils.isNotEmpty(req.getHeader(HttpHeaders.TRANSFER_ENCODING))) { - InputStreamEntity entity = new InputStreamEntity(req.getInputStream(), - Long.parseLong(req.getHeader(HttpHeaders.CONTENT_LENGTH))); - putRequest.setEntity(entity); - } - copyRequestHeaders(req, putRequest); + generateRequestEntity(req, putRequest); putRequest.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BEARER + authData.getAccessToken()); ProxyResponse proxyResponse = execute(putRequest); @@ -156,6 +154,8 @@ public class InvokerHandler extends HttpServlet { } HandlerUtil.handleSuccess(req, resp, serverUrl, platform, proxyResponse); } + } catch (FileUploadException e) { + log.error("Error occurred when processing Multipart PUT request.", e); } catch (IOException e) { log.error("Error occurred when processing PUT request.", e); } @@ -166,7 +166,7 @@ public class InvokerHandler extends HttpServlet { try { if (validateRequest(req, resp)) { HttpDelete deleteRequest = new HttpDelete(generateBackendRequestURL(req)); - copyRequestHeaders(req, deleteRequest); + copyRequestHeaders(req, deleteRequest, false); deleteRequest.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BEARER + authData.getAccessToken()); ProxyResponse proxyResponse = execute(deleteRequest); if (HandlerConstants.TOKEN_IS_EXPIRED.equals(proxyResponse.getExecutorResponse())) { @@ -187,6 +187,49 @@ public class InvokerHandler extends HttpServlet { } } + /** + * Generate te request entity for POST and PUT requests from the incoming request. + * + * @param req incoming {@link HttpServletRequest}. + * @param proxyRequest proxy request instance. + * @throws FileUploadException If unable to parse the incoming request for multipart content extraction. + * @throws IOException If error occurred while generating the request body. + */ + private void generateRequestEntity(HttpServletRequest req, HttpEntityEnclosingRequestBase proxyRequest) + throws FileUploadException, IOException { + if (ServletFileUpload.isMultipartContent(req)) { + ServletFileUpload servletFileUpload = new ServletFileUpload(new DiskFileItemFactory()); + List fileItemList = servletFileUpload.parseRequest(req); + MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create(); + entityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + for (FileItem item : fileItemList) { + if (!item.isFormField()) { + entityBuilder.addPart(item.getFieldName(), new InputStreamBody(item.getInputStream(), + ContentType.create(item.getContentType()), item.getName())); + } else { + entityBuilder.addTextBody(item.getFieldName(), item.getString(), + ContentType.create(item.getContentType())); + } + } + proxyRequest.setEntity(entityBuilder.build()); + copyRequestHeaders(req, proxyRequest, false); + } else { + if (StringUtils.isNotEmpty(req.getHeader(HttpHeaders.CONTENT_LENGTH)) || + StringUtils.isNotEmpty(req.getHeader(HttpHeaders.TRANSFER_ENCODING))) { + InputStreamEntity entity = new InputStreamEntity(req.getInputStream(), + Long.parseLong(req.getHeader(HttpHeaders.CONTENT_LENGTH))); + proxyRequest.setEntity(entity); + } + copyRequestHeaders(req, proxyRequest, true); + } + } + + /** + * Generates the target URL for the proxy request. + * + * @param req incoming {@link HttpServletRequest} + * @return Target URL + */ private String generateBackendRequestURL(HttpServletRequest req) { StringBuilder urlBuilder = new StringBuilder(); urlBuilder.append(serverUrl).append(HandlerConstants.API_COMMON_CONTEXT).append(apiEndpoint); @@ -196,12 +239,22 @@ public class InvokerHandler extends HttpServlet { return urlBuilder.toString(); } - private void copyRequestHeaders(HttpServletRequest req, HttpRequestBase httpRequest) { + /** + * Copy incoming request headers to the proxy request. + * + * @param req incoming {@link HttpServletRequest} + * @param httpRequest proxy request instance. + * @param preserveContentType true if content type header needs to be preserved. + * This should be set to false when handling multipart requests as Http + * client will generate the Content-Type header automatically. + */ + private void copyRequestHeaders(HttpServletRequest req, HttpRequestBase httpRequest, boolean preserveContentType) { Enumeration headerNames = req.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH) || - headerName.equalsIgnoreCase(SM.COOKIE)) { + headerName.equalsIgnoreCase(SM.COOKIE) || + (!preserveContentType && headerName.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE))) { continue; } Enumeration headerValues = req.getHeaders(headerName); @@ -212,33 +265,35 @@ public class InvokerHandler extends HttpServlet { } /*** + * Validates the incoming request. * * @param req {@link HttpServletRequest} * @param resp {@link HttpServletResponse} * @return If request is a valid one, returns TRUE, otherwise return FALSE * @throws IOException If and error occurs while witting error response to client side */ - private static boolean validateRequest(HttpServletRequest req, HttpServletResponse resp) + private boolean validateRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException { serverUrl = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort(); apiEndpoint = req.getPathInfo(); - String sessionAuthDataKey = req.getHeader(HandlerConstants.X_PLATFORM_HEADER); + platform = req.getHeader(HandlerConstants.X_PLATFORM_HEADER); HttpSession session = req.getSession(false); + if (session == null) { log.error("Unauthorized, You are not logged in. Please log in to the portal"); handleError(req, resp, HttpStatus.SC_UNAUTHORIZED); return false; } - if (StringUtils.isEmpty(sessionAuthDataKey)) { + if (StringUtils.isEmpty(platform)) { log.error("\"X-Platform\" header is empty in the request. Header is required to obtain the auth data from" + " session."); handleError(req, resp, HttpStatus.SC_BAD_REQUEST); return false; } - authData = (AuthData) session.getAttribute(sessionAuthDataKey); - platform = (String) session.getAttribute(HandlerConstants.PLATFORM); + authData = (AuthData) session.getAttribute(platform); + if (authData == null) { log.error("Unauthorized, Access token not found in the current session"); handleError(req, resp, HttpStatus.SC_UNAUTHORIZED); @@ -262,7 +317,7 @@ public class InvokerHandler extends HttpServlet { * @return {@link ProxyResponse} if successful and null if failed. * @throws IOException If an error occurs when try to retry the request. */ - private static ProxyResponse retryRequestWithRefreshedToken(HttpServletRequest req, HttpServletResponse resp, + private ProxyResponse retryRequestWithRefreshedToken(HttpServletRequest req, HttpServletResponse resp, HttpRequestBase httpRequest) throws IOException { if (refreshToken(req, resp)) { httpRequest.setHeader(HttpHeaders.AUTHORIZATION, HandlerConstants.BEARER + authData.getAccessToken()); @@ -284,7 +339,7 @@ public class InvokerHandler extends HttpServlet { * @return If successfully renew tokens, returns TRUE otherwise return FALSE * @throws IOException If an error occurs while witting error response to client side or invoke token renewal API */ - private static boolean refreshToken(HttpServletRequest req, HttpServletResponse resp) + private boolean refreshToken(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (log.isDebugEnabled()) { log.debug("refreshing the token"); @@ -347,7 +402,7 @@ public class InvokerHandler extends HttpServlet { * @param errorCode HTTP error status code * @throws IOException If error occurred when trying to send the error response. */ - private static void handleError(HttpServletRequest req, HttpServletResponse resp, int errorCode) + private void handleError(HttpServletRequest req, HttpServletResponse resp, int errorCode) throws IOException { ProxyResponse proxyResponse = new ProxyResponse(); proxyResponse.setCode(errorCode); diff --git a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java index 4a790f18e97..cf2cb4db601 100644 --- a/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java +++ b/components/ui-request-interceptor/io.entgra.ui.request.interceptor/src/main/java/io/entgra/ui/request/interceptor/util/HandlerConstants.java @@ -23,7 +23,6 @@ public class HandlerConstants { public static final String APP_REG_ENDPOINT = "/api-application-registration/register"; public static final String UI_CONFIG_ENDPOINT = "/api/application-mgt/v1.0/config/ui-config"; public static final String TOKEN_ENDPOINT = "/oauth2/token"; - public static final String X_PLATFORM_HEADER = "X-Platform"; public static final String BASIC = "Basic "; public static final String BEARER = "Bearer "; public static final String COLON = ":"; @@ -39,6 +38,8 @@ public class HandlerConstants { public static final String EXECUTOR_EXCEPTION_PREFIX = "ExecutorException-"; public static final String TOKEN_IS_EXPIRED = "ACCESS_TOKEN_IS_EXPIRED"; + public static final String X_PLATFORM_HEADER = "X-Platform"; + public static final int INTERNAL_ERROR_CODE = 500; public static final long TIMEOUT = 1200; } diff --git a/pom.xml b/pom.xml index f3b33c70e1b..736ce68f984 100644 --- a/pom.xml +++ b/pom.xml @@ -1310,6 +1310,11 @@ httpcore ${apache.http.core.version} + + org.apache.httpcomponents + httpmime + ${apache.http.mime.version} + commons-lang.wso2 commons-lang @@ -2137,6 +2142,7 @@ 4.5.6 4.4.10 + 4.5.8 1.9