I'm a software architect with Lockheed Martin Mission Systems and Training. Most of my recent work has been with Java, especially Java EE (JBoss) and OSGi (Karaf), but I've worked with C, C++, C#, Ada, Python, MATLAB, and a few other things over time. Alan is a DZone MVB and is not an employee of DZone and has posted 14 posts at DZone. You can read more from them at their website. View Full User Profile

HTTP and HTTP/S Proxies with Jetty

10.15.2013
| 11325 views |
  • submit to reddit

Introduction

I’ve talked before about Jetty as an embedded servlet container. Jetty also includes some useful utility servlet implementations, one of which isProxyServlet.

ProxyServlet is a way to create an HTTP or HTTP/S proxy in very few lines of code. Even though it’s part of the Jetty project, it’s modularized to be independent of the Jetty server, so you can use it even in cases where the servlet won’t be run in Jetty.

Motivation

Why might you need a proxy servlet? One reason is to address issues raised by the same origin policy. In general, a script loaded from one site is not allowed to make requests from a different site. While it is possible to work around this (for example using JSONP) I tend to think a proxy is a more elegant solution as it doesn’t require exploiting a hole to download and evaluate arbitrary JavaScript.

A proxy might also be useful to allow a user to access a web service without providing all the information necessary to access it. In our example, we’ll be providing a proxy for Google’s Places API without having to send the Google API key down to the browser.

The proxy we’ll be looking at is a per-request proxy, so it’s not something that could conveniently be used for caching remote server responses in case of slow connections or server failures.

Example

The example is part of the Spring WebMVC application I use to present WebMVC and REST for a Java class. I’ve added the PlacesProxyServletand a basic HTML page to demonstrate fetching Google Places search results and using them in jQuery.

Maven POM

To get started, we need jetty-proxy in our pom.xml. Prior to Jetty 9, theProxyServlet class lived in jetty-servlets, but it’s been moved, probably to reduce the other Jetty dependencies that have to be pulled in.

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-proxy</artifactId>
    <version>${jetty.version}</version>
</dependency>

Java

Next, we create a class that extends ProxyServlet. We need to know the right URI to use for Google Places, and we need a Google API key. The best way to handle this is to allow them to be passed in from the servlet context using init-param, but I like to also allow them to be overridden using Java system properties. We start by overriding the init() method:

public void init() throws ServletException {
    super.init();
    ServletConfig config = getServletConfig();
    placesUrl = config.getInitParameter("PlacesUrl");
    apiKey = config.getInitParameter("GoogleApiKey");
    // Allow override with system property
    try {
        placesUrl = System.getProperty("PlacesUrl", placesUrl);
        apiKey = System.getProperty("GoogleApiKey", apiKey);
    } catch (SecurityException e) {
    }
    if (null == placesUrl) {
        placesUrl = "https://maps.googleapis.com/maps/api/place/search/json";
    }
}

To actually proxy the requests, the key method is rewriteURI. Again, this is new to Jetty 9; previously there was a method called proxyHttpURI that accomplished pretty much the same function.

protected URI rewriteURI(HttpServletRequest request) {
    String query = request.getQueryString();
    return URI.create(placesUrl + "?" + query + "&key=" + apiKey);
}

This method returns the “real” URI that the Jetty proxy servlet will call. All of the data from the client request is available. In this case, we just need the browser’s query parameters so we can pass them on to Google Places.

Tweaks

To actually get this to work with the Google Places API, there were a couple other changes required. First, the Places API enforces HTTP/S. Note that this doesn’t mean that our client has to connect to our proxy servlet using HTTP/S; regular HTTP is perfectly fine for that connection because our proxy servlet is making a brand new HTTP/S connection (using Jetty’s HttpClientclass). However, it does mean that we need to tell the Jetty HttpClient that it’s OK to use HTTP/S. We do this by overriding the method that theProxyServlet class uses to make a new HttpClient:

protected HttpClient newHttpClient() {
    SslContextFactory sslContextFactory = new SslContextFactory();
    HttpClient httpClient = new HttpClient(sslContextFactory);
    return httpClient;
}

Second, Google Places didn’t like the fact that the Jetty proxy servlet adds aHost header to the request with the name of the originating server. With this header, the Google Places server returns 404 in response to the request. Fortunately, this is easy to fix; we just have to remove that header before the request goes out. We can do this by overriding the customizeProxyRequestmethod that ProxyServlet thoughtfully provides for just such a problem:

protected void customizeProxyRequest(Request proxyRequest,
        HttpServletRequest request) {
    proxyRequest.getHeaders().remove("Host");
}

Updates to web.xml

To get this servlet up and running, we need to add it to web.xml. In the case of the example application, this required updating to Servlet 3.0, since the Jetty proxy servlet wants to use asynchronous connections. This is a good thing in terms of increasing the number of simulataneous requests the proxy servlet can process, but it requires enabling that feature in web.xml:

<servlet>
    <servlet-name>PlacesProxy</servlet-name>
    <servlet-class>org.anvard.webmvc.server.PlacesProxyServlet</servlet-class>
    <init-param>
      <param-name>GoogleApiKey</param-name>
      <param-value>YOUR_KEY_HERE</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

<servlet-mapping>
    <servlet-name>PlacesProxy</servlet-name>
    <url-pattern>/places</url-pattern>
</servlet-mapping>

The async-supported tag is important; the proxy servlet won’t work without it.

Browser interface

On the browser side, we need a way to query and then display the results. I cannibalized some example HTML and JavaScript I had lying around that did something similar with CometD. (Unfortunately, I can’t find the original source to provide a linkback.) The relevant jQuery part looks like this:

$.getJSON("/places?location=39.016249,-77.122993&radius=1000&types=food&sensor=false",
    function ( data ) {
        console.log( data );
        for (i = 0; i < data.results.length; i++) {
            result =data.results[i];
            $('<li>').html(result.name + '<br>' + result.vicinity).appendTo('#contentList');
        }
    })
    .fail(function() {
        console.log( "error" );
    })
    .always(function() {
        $("#status").text("Complete.");
    });

The jQuery makes an AJAX call to the proxy servlet, which then makes a call to Google Places. The resulting JSON response data is sent through as-is. The (anonymous) “success” function then gets called. It iterates through the returned results, adding <li> tags to the existing list for each result it finds.

Conclusion

Of course, a proxy servlet doesn’t have to be used for sites on the Internet. One of my motivations for creating the example application was to show how easy it was to REST-enable an existing standalone Java application. Many systems that use Java have multiple standalone Java applications, each performing some independent function. This would make it challenging to create a single unified web interface while still allowing each application to define its own REST API. Proxy servlets can help by making it look like there’s a single endpoint for all the various APIs, while not requiring any logic that knows about the contents of the interfaces.

Published at DZone with permission of Alan Hohn, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)