Skip to content

Commit

Permalink
PHEE-413 Fixed the bug while verifying signature in the interceptor (#42
Browse files Browse the repository at this point in the history
)

* PHEE-413 Fixed the bug while verifying signature in the interceptor

* Version upgraded to 1.6.1-rc.1

* Generic logs converted to info for better observability
  • Loading branch information
danishjamal104 authored Sep 4, 2023
1 parent dafcd12 commit beed14d
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 34 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ configure(this) {
}

group = 'org.mifos'
version = '1.5.0-SNAPSHOT'
version = '1.6.1-rc.1'
sourceCompatibility = JavaVersion.VERSION_17
def artifactId = 'ph-ee-connector-common'
def versionNumber = version
Expand Down
89 changes: 60 additions & 29 deletions src/main/java/org/mifos/connector/common/interceptor/JWSUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.mifos.connector.common.channel.dto.PhErrorDTO;
import org.mifos.connector.common.exception.PaymentHubErrorCategory;
import org.mifos.connector.common.util.Constant;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;

@Slf4j
public final class JWSUtil {
Expand Down Expand Up @@ -123,7 +123,7 @@ public static void writeErrorResponse(HttpServletResponse response, PhErrorDTO e
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
try {
response.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
response.setHeader(CONTENT_TYPE, "application/json");
response.getWriter().write(getObjectMapper().writeValueAsString(errorDTO));
} catch (IOException e) {
throw new RuntimeException(e);
Expand All @@ -138,13 +138,13 @@ public static void writeErrorResponse(HttpServletResponse response, PhErrorDTO e
* @return data for verification in form of string
* @throws IOException
* throws as part of input stream
* @throws ServletException
* inherited from functional chain
*/
public static String parseBodyPayload(HttpServletRequest request) throws IOException, ServletException {
public static String parseBodyPayload(HttpServletRequest request) throws IOException {
log.debug("Content-type: {}", request.getHeader(CONTENT_TYPE));
String data = null;
String body = IOUtils.toString(request.getInputStream(), Charset.defaultCharset());
if (body.isEmpty() && !request.getMethod().equals(HttpMethod.GET.name())) {
if (isMultipartRequest(request) &&
!request.getMethod().equals(HttpMethod.GET.name())) {
// parse form data only if body is empty and REQUEST TYPE is not GET
data = parseFormData(request);
} else {
Expand All @@ -154,36 +154,67 @@ public static String parseBodyPayload(HttpServletRequest request) throws IOExcep
return data;
}

// parses the boundary value form the content type
// works if content type is of below kind
// 'multipart/form-data; boundary=--------------------------188270859567880981670528'
public static String parseBoundaryValueFromContentTypeValue(String contentType) {
String[] contentTypeArray = contentType.split("boundary=");
if (contentTypeArray.length < 2) {
return "";
}
return contentTypeArray[1].strip();
}

/**
* Use to parse the string data from the multipart data passed in api
*
* @param httpServletRequest
* @param request
* request object passed to the controller
* @return multipart data in form of string
* @throws ServletException
* @throws IOException
*/
public static String parseFormData(HttpServletRequest httpServletRequest) throws ServletException, IOException {
public static String parseFormData(HttpServletRequest request) throws IOException {
log.debug("Parsing form data");
StringBuilder stringBuilder = new StringBuilder();
Collection<Part> parts;
try {
parts = httpServletRequest.getParts();
if (parts == null || parts.isEmpty()) {
return "";
String contentTypeHeaderValue = request.getHeader(CONTENT_TYPE);
String boundary = parseBoundaryValueFromContentTypeValue(contentTypeHeaderValue);
return parseFormData(request.getInputStream(), boundary);
}

/**
* Use to parse the multipart/form-data from the input stream
*
* @param inputStream
* @param boundary the boundary can be parsed from CONTENT-TYPE header in
* API request header
* @return parsed data in the form of String
* @throws IOException
*/
public static String parseFormData(InputStream inputStream, String boundary) throws IOException {
log.debug("inside MultipartParser");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder partContent = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.contains(boundary)) {
// Read and process the part headers
while ((line = reader.readLine()) != null && !line.isEmpty()) {
// Process part headers if needed
}

// Read and process the part content
while ((line = reader.readLine()) != null && !line.equals(boundary)
&& !line.contains(boundary) && !line.equals("\n") && !line.isEmpty()) {
partContent.append(line);
partContent.append("\n");
}
}
} catch (IOException | ServletException e) {
log.warn("Empty payload in multipart form: {}", e.getLocalizedMessage());
log.error("Empty payload in multipart form", e);
return "";
}
log.debug("HttpServletRequest: {}", parts);
for (Part part : parts) {
log.debug("Part loop");
String partString = IOUtils.toString(part.getInputStream(), Charset.defaultCharset());
stringBuilder.append(partString);
}
return stringBuilder.toString();
return partContent.toString();
}

// returns true if the request is of multipart type
public static boolean isMultipartRequest(HttpServletRequest request) {
return request.getHeader(CONTENT_TYPE).contains("multipart/form-data");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.mifos.connector.common.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.mifos.connector.common.interceptor.helper.MultiReadHttpServletRequest;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Slf4j
public class MultiReadFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.debug("Inside caching filter");
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
chain.doFilter(multiReadRequest, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.servlet.annotation.MultipartConfig;

@Component("JWSMvcConfig")
@Slf4j
@ConditionalOnExpression("${security.jws.enable:false}")
Expand All @@ -28,6 +31,16 @@ public void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
}

@Bean
public FilterRegistrationBean cachingFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new MultiReadFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}


@Bean
@ConditionalOnProperty(prefix = "security.jws.response", name = "enable", havingValue = "true")
public FilterRegistrationBean jwsFilter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class WebSignatureInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// return true means forward this request and false means don;t forward this to controller
log.debug("Request at interceptor");
log.info("Request at interceptor");
if (headerOrder.size() == 0) {
throw new RuntimeException("Header is null");
}
Expand All @@ -45,7 +45,6 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
JWSUtil.writeErrorResponse(response, errorDTO);
return false;
}

String dataToBeHashed = JWSUtil.getDataToBeHashed(request, data, headerOrder);
log.debug("Data to be hashed: {}", dataToBeHashed);

Expand All @@ -62,7 +61,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
}

if (isValidSignature) {
log.debug("Signature is valid");
log.info("Signature is valid");
} else {
log.error("JWS Signature verification failed: {}", signature);
errorDTO = new PhErrorDTO.PhErrorDTOBuilder(PaymentHubError.InvalidJsonWebSignature)
Expand All @@ -72,7 +71,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
response.setStatus(400);
}
response.setHeader(Constant.HEADER_PLATFORM_TENANT_ID, tenant);
log.debug("Request ended at interceptor");
log.info("Request ended at interceptor");
return isValidSignature;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.mifos.connector.common.interceptor.helper;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;

public class ByteArrayServletInputStream extends ServletInputStream {

private final ByteArrayInputStream inputStream;

public ByteArrayServletInputStream(byte[] bytes) {
inputStream = new ByteArrayInputStream(bytes);
}

@Override
public int read() throws IOException {
return inputStream.read();
}

@Override
public boolean isFinished() {
return inputStream.available() == 0;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener readListener) {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.mifos.connector.common.interceptor.helper;

import lombok.Getter;
import lombok.Setter;
import org.springframework.util.StreamUtils;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.Part;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collection;

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {

private byte[] body;

/**
* Creates a ServletRequest adaptor wrapping the given request object.
*
* @param request
* @throws IllegalArgumentException if the request is null
*/
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
try {
body = StreamUtils.copyToByteArray(request.getInputStream());
}catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public ServletInputStream getInputStream() throws IOException {
return new ByteArrayServletInputStream(body);
}

@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.body);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}

@Override
public Collection<Part> getParts() throws IOException, ServletException {
return ((HttpServletRequest) getRequest()).getParts();
}

}

0 comments on commit beed14d

Please sign in to comment.