Friday, 4 September 2009

Coldfusion File Streaming & Stream Proxying

Hot on the heels of yesterdays post about Java Streaming Proxy Servlets comes even more streaming goodness with CF.

The first bit of code opens a file, captures the underlying servlet output stream and then streams out the contents of the file.

streamFile.cfm

<cfsilent>
  <cfif ReFind("[^a-zA-Z0-9_\-\.]", URL.filename)>
    <cfthrow message="not permitted">
  </cfif>

  <cfscript>
    filename = REQUEST.fileDirectory & "/" & URL.filename;
    inputFile = createObject("java", "java.io.File").init(filename);
    inputStream = createObject("java", "java.io.FileInputStream").init(inputFile);
    outputStream = getPageContext().getResponse().getResponse().getOutputStream();

    while(true) {
      b = inputStream.read();

      if(b EQ -1) {
        break;
      }

      outputStream.write(b);
    }
    outputStream.flush();

    inputStream.close();
    outputStream.close();   
  </cfscript>
</cfsilent>

Streaming the contents of a file to a browser is all good because you didn't have to load the whole thing in memory before sending it down the wire. But what if you want to stream a file to a browers that is held on another server, not accessible from the internet. In that case you'll want to stream the file through a proxy to the client.

streamProxy.cfm

<cfsilent>
  <cfheader name="Content-Disposition" value="attachment; filename=#URL.userFilename#">
  <cfcontent type="text/plain">

  <cfif ReFind("[^a-zA-Z0-9_\-\.]", URL.filename)>
    <cfthrow message="not permitted">
  <cfelseif ReFind("[^a-zA-Z0-9_\-\.]", URL.userFilename)>
    <cfthrow message="not permitted">
  </cfif>

  <cfscript>   
    proxy = createObject("java", "org.apache.commons.httpclient.HttpClient").init();

    if(len(CGI.QUERY_STRING) GT 0) {
      query = "?#CGI.QUERY_STRING#";
    } else {
      query = "";
    }

    uri = REQUEST.streamUrl & query;

    proxyMethod = createObject("java", "org.apache.commons.httpclient.methods.GetMethod").init(uri); 
    proxy.executeMethod(proxyMethod); 
    inputStream = proxyMethod.getResponseBodyAsStream(); 
    outputStream = getPageContext().getResponse().getResponse().getOutputStream();
  
    while(true) {
      b = inputStream.read();

      if(b EQ -1) {
        break;
      }

      outputStream.write(b);
    }

    outputStream.flush();

    inputStream.close();
    outputStream.close();
  </cfscript>
</cfsilent>

The proxy uses the Jakarta Commons HttpClient which comes bundled with ColdFusion to call the target stream, passing any URL query parameters supplied.

The response from target tream URL is streamed back to the client via the underlying servlet output stream, just like the file streaming code.

An example project is available for download, make sure to customise the REQUEST scope variables in the Application.cfm file before testing.

Source Code

Thursday, 3 September 2009

Streaming Http Proxy Servlet

You know when you just need a streaming http proxy servlet and you just can't find one? Well try this:

HttpProxyServlet.java

package org.adrianwalker.servlet.proxy;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;

public final class HttpProxyServlet extends HttpServlet {

  private URL url;
  private HttpClient proxy;

  @Override
  public void init(final ServletConfig config) throws ServletException {

    super.init(config);

    try {
      url = new URL(config.getInitParameter("url"));
    } catch (MalformedURLException me) {
      throw new ServletException("Proxy URL is invalid", me);
    }
    proxy = new HttpClient();
    proxy.getHostConfiguration().setHost(url.getHost());
  }

  @Override
  protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
          throws ServletException, IOException {

    Map<String, String[]> requestParameters = request.getParameterMap();

    StringBuilder query = new StringBuilder();
    for (String name : requestParameters.keySet()) {
      for (String value : requestParameters.get(name)) {

        if (query.length() == 0) {
          query.append("?");
        } else {
          query.append("&");
        }

        name = URLEncoder.encode(name, "UTF-8");
        value = URLEncoder.encode(value, "UTF-8");

        query.append(String.format("&%s=%s", name, value));
      }
    }

    String uri = String.format("%s%s", url.toString(), query.toString());
    GetMethod proxyMethod = new GetMethod(uri);
    
    proxy.executeMethod(proxyMethod);
    write(proxyMethod.getResponseBodyAsStream(), response.getOutputStream());
  }

  @Override
  protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
          throws ServletException, IOException {

    Map<String, String[]> requestParameters = request.getParameterMap();

    String uri = url.toString();
    PostMethod proxyMethod = new PostMethod(uri);
    for (String name : requestParameters.keySet()) {
      for (String value : requestParameters.get(name)) {
        proxyMethod.addParameter(name, value);
      }
    }

    proxy.executeMethod(proxyMethod);
    write(proxyMethod.getResponseBodyAsStream(), response.getOutputStream());
  }

  private void write(final InputStream inputStream, final OutputStream outputStream) throws IOException {
    int b;
    while ((b = inputStream.read()) != -1) {
      outputStream.write(b);
    }

    outputStream.flush();
  }

  @Override
  public String getServletInfo() {
    return "Http Proxy Servlet";
  }
}

This class makes use of the Jakarta Commons HttpClient to forward requests to a configurable url and streams back the response.

A sample project using this servlet is available for download below. The servlet acts as a proxy for a google search, returning the search results page. The proxied url is configurable in the web applications web.xml file. The project builds a war and uses the jetty plugin so you can test the code.

Run the project with 'mvn clean install jetty:run-war' and point your brower at http://localhost:8080/http-proxy-servlet

Source Code