Adding a “lite” Groovy web console to a Grails war

Suppose you have a Grails application deployed to a server – how would you go about finding out how the application was configured? If you have the source then you can view Config.groovy, BuildConfig.groovy, etc. (in this case I’m talking about a Grails 2 app but these ideas are generalizable to Grails 3+) but that’s often not enough.

Grails 2 supports external configuration files, which can be in various places and get merged into the final configuration. But just having what you think is the correct source and configuration files isn’t enough since changes could have been made that didn’t make it into source control. And you can’t easily get information from those files in a WAR since they’re compiled into classes.

My preference for digging into a running Grails application is the console plugin, but to use that you would need to add it to BuildConfig.groovy and build and deploy a new WAR, but again that’s not necessarily going to have the same configuration as the previous deployment.

I have a situation like this at work, so I came up with a lightweight way to add a web-based console similar to the console plugin to a WAR. Originally it was a servlet which generated the HTML for a simple form containing a textarea for Groovy code and a submit button to post the code to be run on the server, and the logic (mostly borrowed from the console plugin) to execute the code and return the results to the browser. I compiled it in the same project that the WAR was built from to ensure that it’s compatible with the versions of Groovy, Grails, Spring, etc. and copied the .class file to WEB-INF/classes in the exploded directory in Tomcat’s webapps folder, and manually edited WEB-APP/web.xml to add the required <servlet> and <servlet-mapping> elements, and everything worked great in my small test app.

But when I tried it in the real application I couldn’t access it because of Spring Security. In this particular case I could have worked around that because the app stores Requestmap instances in the database, but I didn’t want to make changes that I might forget to undo, and there’s the chicken-and-egg problem that I don’t necessarily know what the database settings are for this deployment. So instead I converted the servlet to a servlet filter, and made sure to add the filter before the Spring Security filter chain in web.xml and it worked as expected after restarting the server.

I made the changes in the exploded war directory, but it’s also possible to make the changes in the WAR file itself. Since WAR files are ZIP files, you can unzip the WAR, make the changes, and re-zip.

Here’s the source for the filter:

package com.burtbeckwith.hack

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.codehaus.groovy.grails.commons.GrailsApplication
import org.springframework.context.ApplicationContext
import org.springframework.web.context.support.WebApplicationContextUtils

import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@CompileStatic
@Slf4j
class HackFilter implements Filter {

   private ApplicationContext applicationContext
   private GrailsApplication grailsApplication

   void init(FilterConfig fc) throws ServletException {
      applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(fc.servletContext)
      grailsApplication = applicationContext.getBean(GrailsApplication)
   }

   void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req
      HttpServletResponse response = (HttpServletResponse) res

      if ('GET' == request.method) {
         doGet request, response
      }
      else {
         // assume POST
         doPost request, response
      }
   }

   void destroy() {}

   private void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      response.writer.write html(request.contextPath)
   }

   private void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      long startTime = System.currentTimeMillis()

      String code = request.getParameter('code')

      ByteArrayOutputStream baos = new ByteArrayOutputStream()
      PrintStream out = new PrintStream(baos)
      PrintStream systemOut = System.out

      Throwable e
      String result = ''
      try {
         System.out = out
         result = new GroovyShell(grailsApplication.classLoader, new Binding(
               config: grailsApplication.config,
               ctx: applicationContext,
               grailsApplication: grailsApplication,
               out: out,
               request: request,
               session: request.session)).evaluate(code)
      }
      catch (Throwable t) {
         e = t
      }
      finally {
         System.out = systemOut
      }

      if (e) {
         StringWriter sw = new StringWriter()
         e.printStackTrace new PrintWriter(sw)
         result = sw.toString().replace('\t', '   ').replace(System.getProperty('line.separator'), '<br/>\n')
      }

      response.writer << html(request.contextPath, code, """\
Total time: ${System.currentTimeMillis() - startTime}ms

Stdout:
${baos.toString('UTF8')}

${e ? 'Exception' : 'Result'}:
$result""")
   }

   private String html(String contextPath, String code = '', String results = '') {
      """\
<html>
<head>
<title>Hack</title>
</head>
<body>
   <form action="$contextPath/hack" method="POST">
      <span>Code: (binding vars include <i>config</i>, <i>ctx</i>, <i>grailsApplication</i>, <i>out</i>, <i>request</i>, <i>session</i>)</span><br/>
      <textarea name="code" cols="120" rows="25">$code</textarea><br/>
      <input type="submit" value="Execute" name="execute" /><br/>
      <span>Results:</span><br/>
      <textarea name="results" cols="120" rows="25" disabled="disabled">$results</textarea>
   </form>
</body>
</html>
"""
   }
}

and these are the corresponding <filter> and <filter-mapping> elements for web.xml:

<filter>
   <filter-name>hack</filter-name>
   <filter-class>com.burtbeckwith.hack.HackFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>hack</filter-name>
   <url-pattern>/hack</url-pattern>
</filter-mapping>

To access the console, navigate to http://server:port/contextPath/hack. As in the console plugin you can run arbitrary Groovy code (including service method calls, working with domain classes, etc.), and there are several objects in the Binding that you can use – config, ctx, grailsApplication, out, request, and session.

To change the uri from /hack to something else, be sure to update both the <url-pattern> tag in web.xml and the action attribute in the generated form in the filter class.

Leave a Reply

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 License.