Cannot get response body in my filter. Caching enabled.
I have tried many filter implementations - Filter, OncePerRequestFilter and GenericFilterBean. I have also tried writing custom caching mechanism but non of that work. I have already one logging filter working - this filter is executed before the new one and serves for reading request and response. The logging filter works but I want to add another one which reads response and validate it against XSD. Problem is, that logging filter gets the response filled OK but the XML validator filter gets empty string.
My #Controller method just returns Callable. Asynchronous processing may not be problem tho because logger filter works well.
#Component
public class ResposeBodyXmlValidator extends OncePerRequestFilter {
private final XmlUtils xmlUtils;
private final Resource xsdResource;
public ResposeBodyXmlValidator(
XmlUtils xmlUtils,
#Value("classpath:xsd/some.xsd") Resource xsdResource
) {
this.xmlUtils = xmlUtils;
this.xsdResource = xsdResource;
}
#Override
protected void doFilterInternal(
HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain
) throws ServletException, IOException {
ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(httpServletResponse);
doFilter(httpServletRequest, response, filterChain);
if (MediaType.APPLICATION_XML.getType().equals(response.getContentType())) {
try {
xmlUtils.validate(new String(response.getContentAsByteArray(), response.getCharacterEncoding()), xsdResource.getInputStream());
} catch (IOException | SAXException e) {
String exceptionString = String.format("Chyba při volání %s\nNevalidní výstupní XML: %s",
httpServletRequest.getRemoteAddr(),
e.getMessage());
response.setContentType(MediaType.TEXT_PLAIN_VALUE + "; charset=UTF-8");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().print(exceptionString);
}
}
response.copyBodyToResponse(); // I found this needs to be added at the end of the filter
}
}
I expect all my filters to be able to read response body which is cached.
Update
I was mistaken myself. Even LoggerFilter cannot read seponse. I think this has something to do with asynchronous processing of a request.
Related
I have a rest end point designed in spring boot. Tomcat is being used as embedded server. It takes a query parameter.
When I pass query parameter as param1%uFF07 tomcat internally reads parameter as null
When I pass query parameter as param1%FF07 tomcat reads as some character.
tomcat only reads '%' character when followed by two hexadecimal numbers, if u is placed after '%' character tomcat parse parameter as null with message
Character decoding failed. Parameter [name] with value [param1%uFF07]
has been ignored. Note that the name and value quoted here may be
corrupted due to the failed decoding. Use debug level logging to see
the original, non-corrupted values. Note: further occurrences of
Parameter errors will be logged at DEBUG level.
Here is spring boot controller code
#RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
#RequestMapping("/greeting")
public Greeting greeting(#RequestParam(value = "name", required = false) String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
You are passing % sign in your url, but % is symbol in url, to pass % as it is... you will have to pass %25 then it will work as you expected.
So, if you pass %25uFF07 then it will show you %uFF07 as value.
No need to change anything in application.properties or any kind of settings. I have tested this in my project.
Please feel free to ask for any clarification. Hope It Helps.
I found out a way using filters. Basics about filters could be found over here. We can intercept request query string there and use Tomcat UDecoder class to parse the query string and if any exception is thrown we can show response of 400
public class SimpleFilter implements Filter {
private final UDecoder urlDecoder = new UDecoder();
private final Logger logger = LoggerFactory.getLogger(SimpleFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String queryString = httpServletRequest.getQueryString();
if (queryString != null) {
ByteChunk byteChunk = new ByteChunk();
byteChunk.setBytes(queryString.getBytes(), 0, queryString.length());
try {
urlDecoder.convert(byteChunk, true);
} catch (IOException ioException) {
logger.error("Hazarduos character found in request parameter.");
httpServletResponse.setStatus(HttpStatus.BAD_REQUEST.value());
return;
}
}
chain.doFilter(request, response);
}
#Override
public void destroy() {
}
}
Use Case:
We are developing an AEM Closed User Group site where users will need to submit forms which trigger workflows. Since the users are authenticated, part of the workflow payload needs to include the user who initiated the form.
I'm considering using AEM Forms for this, which saves to nodes under /content/usergenerated/content/forms/af/my-site but the user is not mentioned in the payload (only the service user). In this case, there are two service users: workflow-service running the workflow, and fd-service which handled the form processing and initial saving. E.G. the following code called from the workflow step reports 'fd-service'
workItem.getWorkflowData().getMetaDataMap().get("userId", String.class);
To work around this constraint,
Workflow initiated from publish AEM instance: All workflow instances are created using a service user when adaptive forms, interactive communications, or letters are submitted from AEM publish instance. In these cases, the user name of the logged-in user is not captured in the workflow instance data.
I am adding a filter servlet to intercept the initial form submission before the AEM Forms servlet using a request wrapper to modify the request body adding the original userID.
In terms of forms, workflows and launchers.. This is basically the setup I have
https://helpx.adobe.com/aem-forms/6/aem-workflows-submit-process-form.html
I have reviewed the following resources:
How to change servlet request body in java filter?
https://coderanch.com/t/364591/java/read-request-body-filter
https://gitter.im/Adobe-Consulting-Services/acs-aem-commons?at=5b2d59885862c35f47bf3c71
https://helpx.adobe.com/experience-manager/6-4/forms/using/forms-workflow-osgi-handling-user-data.html
Here is the code for my wrapper
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletInputStream;
import java.io.*;
public class FormSubmitRequestWrapper extends SlingHttpServletRequestWrapper {
String requestPayload;
private static final Logger log = LoggerFactory.getLogger(FormSubmitRequestWrapper.class);
public FormSubmitRequestWrapper(SlingHttpServletRequest slingRequest) {
super(slingRequest);
// read the original payload into the requestPayload variable
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
// read the payload into the StringBuilder
InputStream inputStream = slingRequest.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
// make an empty string since there is no payload
stringBuilder.append("");
}
} catch (IOException ex) {
log.error("Error reading the request payload", ex);
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException iox) {
log.error("Error closing bufferedReader", iox);
}
}
}
requestPayload = stringBuilder.toString();
}
/**
* Override of the getInputStream() method which returns an InputStream that reads from the
* stored requestPayload string instead of from the request's actual InputStream.
*/
#Override
public ServletInputStream getInputStream ()
throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestPayload.getBytes());
ServletInputStream inputStream = new ServletInputStream() {
public int read ()
throws IOException {
return byteArrayInputStream.read();
}
};
return inputStream;
}
}
Here is my filter
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.engine.EngineConstants;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Session;
import javax.servlet.*;
import java.io.IOException;
#Component(service = Filter.class,
immediate = true,
property = {
Constants.SERVICE_DESCRIPTION + "=Add the CUG userID to any UGC posts",
EngineConstants.SLING_FILTER_SCOPE + "=" + EngineConstants.FILTER_SCOPE_REQUEST,
Constants.SERVICE_RANKING + ":Integer=3000",
EngineConstants.SLING_FILTER_PATTERN + "=/content/forms/af/my-site.*"
})
public class DecorateUserGeneratedFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(DecorateUserGeneratedFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final SlingHttpServletResponse slingResponse = (SlingHttpServletResponse ) response;
final SlingHttpServletRequest slingRequest= (SlingHttpServletRequest) request;
FormSubmitRequestWrapper wrappedRequest = new FormSubmitRequestWrapper(slingRequest);
log.info("starting ConfirmAlumniStatus workflow");
log.info(getCurrentUserId(slingRequest));
chain.doFilter(wrappedRequest, slingResponse);
}
#Override
public void destroy() {
}
public String getCurrentUserId(SlingHttpServletRequest request) {
ResourceResolver resolver = request.getResourceResolver();
Session session = resolver.adaptTo(Session.class);
String userId = session.getUserID();
return userId;
}
}
When POST submissions get processed by this filter, I'm getting the error below stating the request body has already been read. So it seems the filter ranking might not be high enough.
25.06.2018 13:11:13.200 ERROR [0:0:0:0:0:0:0:1 [1529946669719] POST /content/forms/af/my-site/request-access/jcr:content/guideContainer.af.internalsubmit.jsp
HTTP/1.1] org.apache.sling.engine.impl.SlingRequestProcessorImpl
service: Uncaught Throwable java.lang.IllegalStateException: Request
Data has already been read at
org.apache.sling.engine.impl.request.RequestData.getInputStream(RequestData.java:669)
at
org.apache.sling.engine.impl.SlingHttpServletRequestImpl.getInputStream(SlingHttpServletRequestImpl.java:292)
at
javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:136)
at
my.site.servlets.FormSubmitRequestWrapper.(FormSubmitRequestWrapper.java:26)
at
my.site.servlets.DecorateUserGeneratedFilter.doFilter(DecorateUserGeneratedFilter.java:75)
at
org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:68)
at
org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:73)
at
org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:73)
at
com.cognifide.cq.includefilter.DynamicIncludeFilter.doFilter(DynamicIncludeFilter.java:82)
at
org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:68)
at
org.apache.sling.engine.impl.debug.RequestProgressTrackerLogFilter.doFilter(RequestProgressTrackerLogFilter.java:10
I don't think the service ranking is working. When I view
http://localhost:4502/system/console/status-slingfilter
my filter is listed as shown. Judging from the other filters listed, I think the leftmost number is the filter ranking. For some reason my filter is ranked 0 even though I set is as service.ranking=700
0 : class my.site.servlets.DecorateUserGeneratedFilter (id:
8402, property: service.ranking=700); called: 0; time: 0ms; time/call:
-1µs
Update: I was able to fix the filter rank, making it 700 still gave the IllegalStateException. Making it 3000 made that problem go away. But when request.getInputStream() is called from my wrapper. It returns null.
What you are trying to do might be the easy route, but might not be future-proof for new AEM releases.
You need total control of how your workflow is triggered!:
Your forms should have a field that contains the workflow path (and maybe other information needed for that workflow)
Create a custom servlet that your forms will post to.
In that servlet process all user posted values (from the form). But especially get a hold of the intended workflow path and trigger it using the workflow API.
This way you don't have to mess with launchers and the workflows are triggered by your users using their user id.
Hope this helps.
Right idea, wrong location.
The short answer is that when you implement the SlingHttpServletRequestWrapper it provides a default handling of method calls to the original SlingHttpServletRequest if you're adding a parameter on the fly what you want to do is to make sure that the methods that are interacting with the parameters are overridden so that you can be sure yours is added. So on initialization, call the original parameter map, copy those items in a new map which includes your own values.
Then over ride any methods that would request those values
getParameter(String)
getParameterMap()
getParameterNames()
getParameterValues(String)
Don't touch the InputStream, that's already been processed to obtain any parameters that are being passed in.
Additionally, that is one of two ways you can handle this type of use case, the other option is to use the SlingPOSTProcessors as documented
https://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html
which allows you to detect what is being written to the repository and modify the data to include, like your case, an additional field.
if you are looking for code example :
#SlingFilter(order = 1)
public class MyFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
return;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
ServletRequest request = servletRequest;
if (servletRequest instanceof SlingHttpServletRequest) {
final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) servletRequest;
request = new SlingHttpServletRequestWrapper(slingRequest) {
String userId = getCurrentUserId(slingRequest);
};
}
filterChain.doFilter(request, servletResponse);
}
#Override
public void destroy() {
return;
}
I'm using Dropwizard 0.8.0 that comes with Jetty-Jersey-Jackson stack.
For security reasons, I want to add a filter that makes every request that passes through a particular route defined with Jersey returns always 200, even in case of error (4xx, 5xx, etc.).
Is this possibile with Jetty/Servlet filters? Can I intercept the request after it passed through Jersey resource (the controller), but before it is returned to client, in order to modify the http status code?
UPDATE:
I'm trying to do this with a ServletFilter, but it seems that the response is sent to client before my code is executed.
I've written the filter this way:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
if (response instanceof HttpServletResponse) {
/* No pre-processing */
chain.doFilter(request, response);
/* Post-processing: */
HttpServletResponse modifiedResponse = (HttpServletResponse) response;
if (modifiedResponse.getStatus() != 200) {
modifiedResponse.setStatus(200);
}
}
}
With this, registered in Dropwizard with:
environment.servlets().addFilter("MyCustomFilter", new MyCustomFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/mypath/*");
The filter is executed, and in the access.log I see all the requests with status code 200; but the client always get the "real" status code (ex, a 404, or a "Method not allowed").
It seems that the response is sent to client before the last part of filter is executed. Moreover, I can't modify the response body. I tried also with a HttpServletResponseWrapper, but with no luck.
You could create your custom javax.ws.rs.ext.ExceptionMapper<RuntimeException>. In that case, every exception that you get from your server, you can resolve it to 200.
Check this guy out. It should be easy to integrate with what you need.
Instead of what the tutorial shows you, like this example:
if (webAppException.getResponse().getStatus() == 401) {
return Response
.status(Response.Status.UNAUTHORIZED)
.entity(new PublicFreemarkerView("error/401.ftl"))
.build();
}
You code will be
if (webAppException.getResponse().getStatus() == 401) {
return Response
.status(Response.Status.OK)
.build();
}
I'm updating the question with my solution that finally works: I've used a Jersey filter instead of a Jetty http filtering, in order to manage directly the Response object from Jersey.
Obviously, this works only if you're using Jetty+Jersey, not with Jetty stand-alone.
Here's the filter I'm using:
/**
* When active, this filter transforms all responses for specified basePath to 200, even in case of error.
*/
#Provider
public class DiscardErrors implements ContainerResponseFilter
{
private String basePath;
public DiscardErrors(String basePath)
{
this.basePath = basePath;
}
#Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException
{
if containerRequestContext.getUriInfo().getPath().startsWith(this.basePath) {
if (containerResponseContext.getStatus() != 200) { // Check if response code is different from 200
containerResponseContext.setStatus(200); // Force 200 status code
containerResponseContext.setEntity(""); // Empty body
}
}
}
}
And register it in Dropwizard:
environment.jersey().register(new DiscardErrors("/"));
I have a Jersey 2 application containing resources that consume and produce json. My requirement is to add a signature to an Authorization response header generated from a combination of various piece of response data (similar to the Amazon Webservices request signature). One of these pieces of data is the response body but I cant see that there are any filter or interception points that will allow me access to the json content. I imagine this is mainly because the response outputstream is for writing not reading.
Any ideas as to how I can read the response body - or alternative approaches ?
Thank you.
My understanding is that when your application is responding to a request, you want to modify the Authorization header by adding a signature to it's value.
If that's the case, you want to implement a ContainerResponseFilter:
public class MyContainerResponseFilter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext containerRequestContext, ContainerResponseContext containerResponseContext) throws IOException {
// You can get the body of the response from the ContainerResponseContext
Object entity = containerResponseContext.getEntity();
// You'll need to know what kind of Object the entity is in order to do something useful though
// You can get some data using these functions
Class<?> entityClass = containerResponseContext.getEntityClass();
Type entityType = containerResponseContext.getEntityType();
// And/or by looking at the ContainerRequestContext and knowing what the response entity will be
String method = containerRequestContext.getMethod();
UriInfo uriInfo = containerRequestContext.getUriInfo();
// Then you can modify your Authorization header in some way
String authorizationHeaderValue = containerResponseContext.getHeaderString(HttpHeaders.AUTHORIZATION);
authorizationHeaderValue = authorizationHeaderValue + " a signature you calculated";
containerResponseContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, authorizationHeaderValue);
}
}
Be warned that the filter function will be called for all requests to your application, even when Jersey couldn't find a matching resource for the request path, so you may have to do some extra checking.
You can implement ContainerRequestFilter in order to access the content, and once you are finished with your interception logic, forward it to the request. E.g.
import java.io.*;
import com.sun.jersey.api.container.ContainerException;
import com.sun.jersey.core.util.ReaderWriter;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
public class ExampleFilter implements ContainerRequestFilter {
#Override
public ContainerRequest filter(ContainerRequest req) {
try(InputStream in = req.getEntityInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) {
if (in.available() > 0) {
StringBuilder content = new StringBuilder();
ReaderWriter.writeTo(in, out);
byte[] entity = out.toByteArray();
if (entity.length > 0) {
content.append(new String(entity)).append("\n");
System.out.println(content);
}
req.setEntityInputStream(new ByteArrayInputStream(entity));
}
} catch (IOException ex) {
//handle exception
}
return req;
}
}
I am trying to use RequestBuilder in GWT to see if Accept-Ranges is supported.
Following is my client code:
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,pathToServlet);
builder.setHeader("Range","bytes=0-10");
RequestCallback callback = new RequestCallback() {
#Override
public void onError(Request arg0, Throwable arg1) {
}
#Override
public void onResponseReceived(Request req, Response res) {
log.info("Text:"+res.getText());
log.info("Code:"+res.getStatusCode());
}
};
try {
builder.sendRequest(null, callback);
} catch (RequestException e) {}
And my servlet code is just a simple test code:
public class RangeTest extends HttpServlet{
static Logger log = Logger.getLogger(RangeTest.class);
#Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
String output = new String("This is a test string to be sent to the client");
response.setContentType("text/xml");
PrintWriter out = response.getWriter();
out.println(output);
}
}
In the output I get on the client, following is printed:
Text:This is a test string to be sent to the client
Code:200
What I expected was, since I gave the range header as 0-10 in the request, only the first 10 bytes will be sent to the client. But here the entire string is getting sent. What am I doing wrong here? Is there anything I have missed?
I feel my comment is more readable for other as answer (and effectively it is one):
You are not evaluating the range-header in your servlet-method. And the super class HttpServlet does not evaluate it either (but DefaultServlet from Tomcat).
The servlet specification has left most of the implementation work to providers like Apache. This explains why API classes like HttpServlet does not do the work of interpreting special http headers, but provider classes like the mentioned Tomcat-DefaultServlet. The main purpose of a specification is mainly to enable different implementations not to force people to only one.