Friday, March 18, 2011

Servlet http proxy

For a while I have been working on a project where we are trying to integrate a windows-based service in a Java EE environment. Basically the problem is to host an Ajax application under an application server like IBM Webspehere or Apache Tomcat. We also want to support portlets following the JSR 268 specifikation.

I googled around a bit and found lots of useful stuff, but nothing that did all that we wanted. The mainly problems was with headers and cookies, since we need to keep a session to the servicein the background we need to make those work. So I ended up writing my own. Not much code, but it might be useful for someone else, as it is pretty general stuff.

The service method

Since we want to handle all HTTP calls, we override the service method of HttpServlet. If you want only some methods (GET and PUT for example) to work you might just override doGet and doPost for example, but we want it all.. The implementation looks like this:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String qry = req.getQueryString();
URL url = new URL(protocol, host, port, req.getRequestURI()
+ (qry == null ? "" : "?" + qry));
HttpURLConnection urlConnection = (HttpURLConnection) url
handleRequestHeaders(req, urlConnection);
handlePostData(req, urlConnection);
// make the call - get response code
int responseCode = urlConnection.getResponseCode();

if (responseCode != HttpURLConnection.HTTP_OK)"HTTP code:" + responseCode + " "
+ urlConnection.getResponseMessage() + " URL["
+ url.toString() + "] " + req.getMethod());
log.debug("HTTP code:" + responseCode + " URL[" + url.toString()
+ "] " + req.getMethod());
handleResponseHeaders(resp, urlConnection);
// send output to client
handleContent(resp, urlConnection, responseCode);

This is pretty straight forward. The protocol (http/https), server and port to use are configurable. We take the URI from the request and add the query string, if there is one. Together with the configure host etc, this forms the new URL, which we open, using the same method as in the call. We set some parameters, request headers and post data if there is any.

We then call the URL, check the HTTP response code, and take care of the headers and the response, which all are sent to the client.

Handle headers
The part that we really needed was the headers. This simply transfers the headers between the two connections, with a few exceptions:

private void handleRequestHeaders(HttpServletRequest req,
HttpURLConnection urlConnection) {
// set request headers
Enumeration e = req.getHeaderNames();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
if (!"Host".equalsIgnoreCase(key)
&& !"Accept-Encoding".equalsIgnoreCase(key)) {
String value = req.getHeader(key);
log.debug("Request Header: " + key + ": " + value);
urlConnection.setRequestProperty((String) key, value);
//add your own headers here

There are two headers that we dont want to transfer from the client's request to our new request:
  1. The Host header, since this will be our proxy server. The new Host header will be added automatically, so we don't need to think about it.
  2. The Accept-Encoding header. This is a fix, the Accept-Encoding header turns gzip on, and since we have not implemented gzip in our proxy we don't want it. Another alternative would be to turn gzipping of requests off in our web server, perhaps this would have been better. If we do transfer th Accept-Encoding header, both servers might zip the content, which will not work.
Finally in this method we add our own headers, which is basically the reason why we need the proxy at all.

When we get the response back from the server we have the corresponding method to handle the response headers:

private void handleResponseHeaders(HttpServletResponse resp,
HttpURLConnection urlConnection) {
// set response headers
Map<string,>> headers = urlConnection.getHeaderFields();
Set<map.entry<string,>>> entrySet = headers.entrySet();
for (Map.Entry<string,>> entry : entrySet) {
String key = entry.getKey();
List<string>> headerValues = entry.getValue();
for (String value : headerValues) {
if (key != null && !"Server".equalsIgnoreCase(key)) {
log.debug("Response Header: " + key + ": " + value);
resp.addHeader(key, value);

This is basically the same as the request headers. In this case we just filter the Server header, where our proxy application server will add its own name.

Finally to set it up in web.xml we configure the URLs we want the proxy to handle, set the protocol, host and port and we are ready to go.

No comments: