views:

3716

answers:

3

I am implementing a RESTful API in Grails, and use a custom authentication scheme that involves signing the body of the request (in a manner similar to Amazon's S3 authentication scheme). Therefore, to authenticate the request, I need to access the raw POST or PUT body content to calculate and verify the digital signature.

I am doing authentication in a beforeInterceptor in the controller. So I want something like request.body to be accessible in the interceptor, and still be able to use request.JSON in the actual action. I am afraid if I read the body in the interceptor using getInputStream or getReader (methods provided by ServletRequest), the body will appear empty in the action when I try to access it via request.JSON.

I am migrating from Django to Grails, and I had the exact same issue in Django a year ago, but it was quickly patched. Django provides a request.raw_post_data attribute you can use for this purpose.

Lastly, to be nice and RESTful, I'd like this to work for POST and PUT requests.

Any advice or pointers would be greatly appreciated. If it doesn't exist, I'd prefer pointers on how to implement an elegant solution over ideas for quick and dirty hacks. =) In Django, I edited some middleware request handlers to add some properties to the request. I am very new to Groovy and Grails, so I have no idea where that code lives, but I wouldn't mind doing the same if necessary.

+2  A: 

It is possible by overriding the HttpServletRequest in a Servlet Filter.

You need to implement a HttpServletRequestWrapper that stores the request body: src/java/grails/util/http/MultiReadHttpServletRequest.java

package grails.util.http;

import org.apache.commons.io.IOUtils;

import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletInputStream;
import java.io.*;
import java.util.concurrent.atomic.AtomicBoolean;

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {

    private byte[] body;

    public MultiReadHttpServletRequest(HttpServletRequest httpServletRequest) {
        super(httpServletRequest);
        // Read the request body and save it as a byte array
        InputStream is = super.getInputStream();
        body = IOUtils.toByteArray(is);
    }

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

    @Override
    public BufferedReader getReader() throws IOException {
        String enc = getCharacterEncoding();
        if(enc == null) enc = "UTF-8";
        return new BufferedReader(new InputStreamReader(getInputStream(), enc));
    }

    private class ServletInputStreamImpl extends ServletInputStream {

        private InputStream is;

        public ServletInputStreamImpl(InputStream is) {
            this.is = is;
        }

        public int read() throws IOException {
            return is.read();
        }

        public boolean markSupported() {
            return false;
        }

        public synchronized void mark(int i) {
            throw new RuntimeException(new IOException("mark/reset not supported"));
        }

        public synchronized void reset() throws IOException {
            throw new IOException("mark/reset not supported");
        }
    }

}

A Servlet Filter that overrides the current servletRequest: src/java/grails/util/http/MultiReadServletFilter.java

package grails.util.http;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Set;
import java.util.TreeSet;

public class MultiReadServletFilter implements Filter {

    private static final Set<String> MULTI_READ_HTTP_METHODS = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER) {{
        // Enable Multi-Read for PUT and POST requests
            add("PUT");
            add("POST");
    }};

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(servletRequest instanceof HttpServletRequest) {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            // Check wether the current request needs to be able to support the body to be read multiple times
            if(MULTI_READ_HTTP_METHODS.contains(request.getMethod())) {
                // Override current HttpServletRequest with custom implementation
                filterChain.doFilter(new MultiReadHttpServletRequest(request), servletResponse);
                return;
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }
}

Then you need to run grails install-templates and edit the web.xml in src/templates/war and add this after the charEncodingFilter definition:

<filter>
    <filter-name>multireadFilter</filter-name>
    <filter-class>grails.util.http.MultiReadServletFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>multireadFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

You should then be able to call request.inputStream as often as you need.

I haven't tested this concrete code/procedure but I've done similar things in the past, so it should work ;-)

Note: be aware that huge requests can kill your application (OutOfMemory...)

Siegfried Puchbauer
I still can't seem to get this working correctly.1) I know the filters are set up correctly, because in an action, "request" has a string representation of "grails.util.http.MultiReadHttpServletRequest@43e1542f".2) I've tried generating the request with both wget --post-data='xxx' and curl -d @file, and verified the request is properly formed from both using Wireshark.3) However, no matter what I try, request.reader.readLine() returns null.4) I also added a "bodyToString()" which uses the body byte array, but it always returns a empty string.Any ideas?
Mickey Ristroph
I've changed the code of the multireadhttpservletrequest to work more reliable. You might want to try that.
Siegfried Puchbauer
Great solution! Helped me when catching errors and sending error report.
Trick
+2  A: 

As can be seen here

http://jira.codehaus.org/browse/GRAILS-2017

just turning off grails automatic handling of XML makes the text accessible in controllers. Like this

class EventsController {   

static allowedMethods = [add:'POST']

def add = {
    log.info("Got request " + request.reader.text)     
    render "OK"
}}

Best, Anders

anders.norgaard
A: 

Stack Overflow won't let me comment on the filter answer (why?) but for those finding it returns an empty input stream, make sure you are defining both the filter and the filter mapping early enough in web.xml. I experienced the same issue and found it went away by making the MultiReadServletFilter be defined first in the filter declarations and first in the filter mappings.

Simonz