Tuesday, July 27, 2010

Slow ICEFaces

ICEfaces (http://www.icefaces.org) is a great JSF framework. I recently developed an application using ICEfaces. On my development machine, everything worked well. However, when I deployed the application to our Linux test server, the whole application became very slow (I'm talking about 20 seconds per request).
After profiling the application, I found out that ICEfaces wraps each HttpServletRequest object to have a uniform interface over all the different Servlet specification versions. It caches some of the values from the HttpServletRequest, and there lies the culprit: one of the values it caches is the getLocalName() value. How this method is implemented depends on your Servlet container, but on apache it results in a call to InetAddress.getHostName(). InetAddress.getHostName() will perform a reverse DNS lookup on its IP address. And there lies the problem: our test server doesn't have a DNS entry, so this method fails (after trying for a very long time). Java will also not cache failed DNS lookup values (you can enable this, see http://download.oracle.com/docs/cd/E17476_01/javase/1.5.0/docs/guide/net/properties.html, but I wouldn't recommend doing so).

So there are 2 possible solutions:
  1. Add a DNS entry for your server (either in your DNS server or in the etc/hosts file or similar)
  2. Sometimes modifying the DNS is not possible (for any number of reasons). Another solution would be to create a Filter that overrides the default behavior of HttpServletRequest.getLocalName(). Here's an example:

public class LocalNameFilter implements Filter {

public void destroy() {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest hsr = (HttpServletRequest) request;  
ServletRequestWrapper wrapper = new HttpServletRequestWrapper(hsr) {
@Override
public String getLocalName() {    
return "myHost";
}
};
chain.doFilter(wrapper, response);
}

public void init(FilterConfig fConfig) throws ServletException {
}

}

Don' forget to declare this filter in your web.xml:
<filter>  
<display-name>LocalNameFilter</display-name>
<filter-name>LocalNameFilter</filter-name>
<filter-class>com.xxx.web.LocalNameFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>LocalNameFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Saturday, July 24, 2010

Avoiding the "javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated" with HttpClient

Please note: this post focuses on the Apache HttpClient library. If you're using plain java, please see this post.

When developing a https application, your test server often doesn't have a (valid) SSL certificate. This will cause the following exception to be thrown when connecting your client to the test server: "javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated".

I will be discussing a way to fix this issue with the apache HttpClient, version 4.0.1 (http://hc.apache.org/httpcomponents-client/).

1. Bits and pieces


You usually create your HttpClient like this:

client = new DefaultHttpClient();

We will need to tell the client to use a different TrustManager. A TrustManager (http://download.oracle.com/docs/cd/E17476_01/javase/1.5.0/docs/api/javax/net/ssl/TrustManager.html) is a class that checks if given credentials (or certificates) are valid. The scheme used by SSL is called X.509 (http://en.wikipedia.org/wiki/X.509), and Java has a specific TrustManager for this scheme, called X509TrustManager. First thing we will need to do is create such a TrustManager:

X509TrustManager tm = new X509TrustManager() {

public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return null;
}
};

As you can see, this code doesn't do much: if a certificate is invalid the TrustManager is supposed to throw a CertificateException in the checkXXX methods. Since we always want to accept all certificates, we never throw an exception.

Next we need to find a way to set this TrustManager in our HttpClient. The TrustManager is used by the SSL sockets. Sockets are created using a SocketFactory. For SSL sockets this is an SSLSocketFactory (http://download.oracle.com/docs/cd/E17476_01/javase/1.5.0/docs/api/javax/net/ssl/SSLSocketFactory.html). When creating a new SSLSocketFactory, you need to pass an SSLContext to the constructor. It is this SSLContext that will contain our newly created TrustManager.

First thing we need to do is get an SSLContext:

SSLContext ctx = SSLContext.getInstance("TLS");

TLS is the successor to SSL, but they use the same SSLContext.

Then we initialize this context with our new TrustManager that we created above:

ctx.init(null, new TrustManager[]{tm}, null);

We can then finally create our SSLSocketFactory:

SSLSocketFactory ssf = new SSLSocketFactory(ctx);

Now we still need to register this SSLSocketFactory with our HttpClient. This is done in the SchemeRegistry of the ConnectionManager of the HttpClient:

ClientConnectionManager ccm = base.getConnectionManager();
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme("https", ssf, 443));

We register a new Scheme, with the protocol https, our newly created SSLSocketFactory which contains our TrustManager and we tell the HttpClient that the default port for https is port 443.

2. Putting it all together


The following class takes a HttpClient and returns a new HttpClient that accepts any SSL certificate:

/*
This code is public domain: you are free to use, link and/or modify it in any way you want, for all purposes including commercial applications. 
*/
public class WebClientDevWrapper {

public static HttpClient wrapClient(HttpClient base) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {

public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[]{tm}, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = base.getConnectionManager();
SchemeRegistry sr = ccm.getSchemeRegistry();
sr.register(new Scheme("https", ssf, 443));
return new DefaultHttpClient(ccm, base.getParams());
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}

You can then do something like this in the code that creates the HttpClient:
this.client = new DefaultHttpClient();
if(dev) {
this.client = WebClientDevWrapper.wrapClient(client);
}

Update

In some exceptional cases, the method described above doesn't work. This is due to the Apache AllowAllHostnameVerifier still being to strict. In this case, you will need your own X509HostnameVerifier. Create it as follows:
X509HostnameVerifier verifier = new X509HostnameVerifier() {

                @Override
                public void verify(String string, SSLSocket ssls) throws IOException {
                }

                @Override
                public void verify(String string, X509Certificate xc) throws SSLException {
                }

                @Override
                public void verify(String string, String[] strings, String[] strings1) throws SSLException {
                }

                @Override
                public boolean verify(String string, SSLSession ssls) {
                    return true;
                }
            };
Then set it on your socket factory:
ssf.setHostnameVerifier(verifier);
If we put everything together, the new code looks like this:
/*
This code is public domain: you are free to use, link and/or modify it in any way you want, for all purposes including commercial applications. 
*/
public class WebClientDevWrapper {

    public static HttpClient wrapClient(HttpClient base) {
        try {
            SSLContext ctx = SSLContext.getInstance("TLS");
            X509TrustManager tm = new X509TrustManager() {

                public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };
            X509HostnameVerifier verifier = new X509HostnameVerifier() {

                @Override
                public void verify(String string, SSLSocket ssls) throws IOException {
                }

                @Override
                public void verify(String string, X509Certificate xc) throws SSLException {
                }

                @Override
                public void verify(String string, String[] strings, String[] strings1) throws SSLException {
                }

                @Override
                public boolean verify(String string, SSLSession ssls) {
                    return true;
                }
            };
            ctx.init(null, new TrustManager[]{tm}, null);
            SSLSocketFactory ssf = new SSLSocketFactory(ctx);
            ssf.setHostnameVerifier(verifier);
            ClientConnectionManager ccm = base.getConnectionManager();
            SchemeRegistry sr = ccm.getSchemeRegistry();
            sr.register(new Scheme("https", ssf, 443));
            return new DefaultHttpClient(ccm, base.getParams());
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

You can then do something like this in the code that creates the HttpClient:
this.client = new DefaultHttpClient();
if(dev) {
this.client = WebClientDevWrapper.wrapClient(client);
}

Friday, July 23, 2010

Setting the notifyURL for the PayPal IPN

In the previous post we saw how to implement a PayPal IPN listener. This is a quick post to show you how to set this listener using PayPal Website Payments Standard.

First you create your "Buy Now" (or "Add to Cart", "Subscribe"...) button. You can do this at https://www.paypal.com/cgi-bin/webscr?cmd=_wp-standard-overview-outside. This wizard creates a form with a couple of hidden parameters. It doesn't set the notify url however, which is the url PayPal IPN needs to call to notify you the user made a purchase. You can add this url to the form as follows:

<input type="hidden" name="notify_url" value="<h:outputText value="#{paypalBean.notifyURL}"/>" />

This sample uses JSF to create the URL, but you can use any tag here (or use a hardcoded URL).

Here is the code that generates the URL:

public String getNotifyURL() {
HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
String nUrl = getPath("PaypalListenerServlet");
return response.encodeURL(nUrl);
}

public final static String getPath(String path) {
HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();
StringBuffer url = new StringBuffer();
String s = request.getProtocol();
url.append(s.substring(0, s.indexOf('/')).toLowerCase());
url.append("://");
url.append(request.getServerName());
url.append(":");
url.append(request.getServerPort());
url.append(request.getContextPath());
if (path.charAt(0) != '/') {
url.append("/");
}
url.append(path);
return url.toString();
}

PayPal Instant Payment Notification (IPN) Servlet with HttpClient 4

Instant Payment Notification (IPN) is PayPal's message service that sends a notification when a transaction is affected. Basically it's a url that gets called by PayPal once a transaction has been successfully completed. This allows your code to do something (send a mail, write to a log...) once the user has made a purchase to PayPal. See https://www.paypal.com/ipn for more information.

Here's how it works: once the user has completed his transaction, PayPal will call this URL for you. A number of parameters (including transaction id, amount, currency...) are passed. What you have to do is create a new HTTP POST request to PayPal with the exact same parameters that PayPal passed to you. If the request originated from the PayPal servers, PayPal will reply with "VERIFIED", else it will send "INVALID". This mechanism is put into place to prevent anyone else than PayPal itself calling your IPN URL and faking a transaction. See https://www.paypal.com/us/cgi-bin/webscr?cmd=p/acc/ipn-info-outside for more information.

There are different ways this can be implemented. PayPal has some java samples available for download (https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/library_download_sdks). In these samples, they implement the IPN as a JSP page. Not only is this ugly (they put plain java code in JSP's), it's also inefficient: there is no output for the user (since this IPN isn't called by a browser), so there is no page to show. It's much better to implement this as a servlet. I used the latest HttpClient (4.0.1 at the time of this writing, http://hc.apache.org/httpcomponents-client/index.html) to implement the callback to PayPal.

Here's the code:

public class PaypalListenerServlet extends HttpServlet {

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(Constants.PAYPAL_URL);
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("cmd", "_notify-validate")); //You need to add this parameter to tell PayPal to verify
for (Enumeration<String> e = request.getParameterNames(); e.hasMoreElements();) {
String name = e.nextElement();
String value = request.getParameter(name);
params.add(new BasicNameValuePair(name, value));
}
post.setEntity(new UrlEncodedFormEntity(params));
String rc = getRC(client.execute(post)).trim();
if ("VERIFIED".equals(rc)) {
//Your business code comes here
}
}

private String getRC(HttpResponse response) throws IOException, IllegalStateException {
InputStream is = response.getEntity().getContent();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String result = "";
String line = null;
while ((line = br.readLine()) != null) {
result += line;
}
return result;
}

}

I create a new HttpClient for each request. Do NOT use a single HttpClient per servlet: although HttpClient IS thread-safe, it only uses one thread, meaning it can only process one request at a time. If you're worried about the overhead of creating a new HttpClient for each request (I wouldn't be) you can still consider binding one in the session context.