Advanced HTTP Techniques
-
font size
decrease font size
increase font size
6.1 The Decorator Approach
One way to enhance the MIDP standard HTTP I/O is to provide a decorator class (CustomConnection) that wraps around the default MIDP HttpConnection implementation but overrides some methods to handle custom headers. Since the decorator class also implements the HttpConnection interface, it is transparent to existing MIDP applications that use HttpConnection.
6.1.1 The CustomConnector Factory Class
In order to instantiate the CustomConnection decorator, we need to write a new connection factory class CustomConnector (Listing 6.1). The custom request headers are set in the CustomConnector.open() method when a new connection is established.
Listing 6.1. The CustomConnector factory class
// In CustomConnector class
public static HttpConnection open(String url) throws IOException {
HttpConnection c = (HttpConnection) Connector.open(url);
setRequestHeaders(c);
c.setRequestProperty("User-Agent",
"Profile/MIDP-1.0, Configuration/CLDC-1.0");
c.setRequestProperty("Content-Language", "en-US");
CustomConnection sc = new CustomConnection(c);
return sc;
}
private static void setRequestHeaders(HttpConnection c) {
// Generate custom header for the request and
// set the headers to the connection object.
}
private static void getResponseHeaders(HttpConnection c) {
// Retrieve headers from the response stream
// and process it.
}
6.1.2 The CustomConnection Class
Now, let's have a closer look at class CustomConnection (Listing 6.2). It overrides only two methods, openInputStream() and openDataInputStream(), which process custom headers when the response data is retrieved.
Listing 6.2. The CustomConnection class
class CustomConnection implements HttpConnection {
private HttpConnection c;
public CustomConnection(HttpConnection c) {
this.c = c;
}
public String getURL() {
return c.getURL();
}
public String getProtocol() {
return c.getProtocol();
}
public String getHost() {
return c.getHost();
}
// More HttpConnection methods
public InputStream openInputStream() throws IOException {
CustomConnector.getResponseHeaders(c);
return c.openInputStream();
}
public DataInputStream openDataInputStream() throws IOException {
CustomConnector.getResponseHeaders(c);
return c.openDataInputStream();
}
}
6.1.3 Decorator Pros and Cons
The decorator solution is elegant and transparent to existing applications. However, it has several weaknesses.
-
It is not scalable. For each task involving custom HTTP headers, we need to write a pair of decorator and connector factory classes.
-
The decorator solution does not work correctly with HTTP tasks that require automatic header resubmission from the client side. An example of such tasks is the HTTP Digest Authentication (see Section 6.5).
For general-purpose HTTP headers handling, we need a new framework that is more powerful than simple decorators.
6.2 The Process-Chain Approach
In this section, I present a process-chain-based HTTP transport framework that works on all J2ME platforms. For simplicity, the new transport class treats all requests and responses as byte arrays rather than streams. If your application requires stream I/O (e.g., a SAX XML parser), you can easily wrap a ByteArrayInputStream or ByteArrayOutputStream around those arrays. The key components in the new framework are listed in Table 6.1.

6.2.1 The HttpClient Source Code
The source code of the HttpClient class is shown in Listing 6.3. Notice how we walk through the handlers chain twice to process both the request and response headers in the query() method. The maxIteration property is used to prevent infinite loops in case of failed challenge-response cycles.
Listing 6.3. The HttpClient class
public class HttpClient {
private String url;
private String requestMethod;
private Vector handlers = new Vector ();
// Max number of challenge/response cycles.
private int maxIteration = 3;
public HttpClient() {}
public void setUrl (String url) {
this.url = url;
}
public void setRequestMethod (String method) {
this.requestMethod = method;
}
public void setMaxIteration (int n) {
maxIteration = n;
}
public void addHandler (Handler h) throws Exception {
handlers.addElement(h);
}
public void removeAllHandlers () throws Exception {
handlers = new Vector ();
}
public byte [] query (byte [] req) throws Exception {
boolean needConnect = true;
HttpConnection c = null;
int currentIteration = 0;
while (needConnect) {
currentIteration++;
if (currentIteration > maxIteration)
throw new Exception("Too many Iterations");
needConnect = false;
if ( c != null ) {
try {
c.close();
} catch (Exception ignore) {
}
}
c = (HttpConnection) Connector.open (url);
c.setRequestMethod( requestMethod );
for (int i = 0; i < handlers.size(); i++)
((Handler) handlers.elementAt(i)).prepareHeaders(c);
c.setRequestProperty("User-Agent",
"Profile/MIDP-1.0, Configuration/CLDC-1.0");
c.setRequestProperty("Content-Language", "en-US");
if ( req != null ) {
OutputStream os = c.openOutputStream ();
os.write(req);
os.close();
}
for (int i = 0; i < handlers.size(); i++) {
needConnect =
((Handler) handlers.elementAt(i)).processHeaders(c) | | needConnect;
}
}
InputStream is = c.openInputStream ();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[256];
while (true) {
int rd = is.read(buf, 0, 256);
if (rd == -1) break;
bos.write(buf, 0, rd);
}
buf = bos.toByteArray();
is.close();
c.close();
return buf;
}
}
Now, let's look at how to use those two frameworks to handle HTTP headers in the real world.
6.3 Session Tracking via HTTP Cookies
Cookies are pieces of NAME=VALUE formatted text embedded in HTTP headers. They are used to track client states. Since cookies reside in HTTP headers, they are transparent to applications and users. The server assigns new cookies to the client through the HTTP header set-cookie. The set-cookie header takes the following format:
set-cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure
The first NAME=VALUE is the cookie itself and is required. All the following attributes, such as expiration time, domain, and path, are optional. When the client makes subsequent requests, it sends the cookies back in the cookies header to identify itself.
cookie: NAME1=VALUE1; NAME1=VALUE2; ...
Note
The server can send out multiple cookies in one connection using multiple set-cookie headers. The client can send back multiple cookies in one header by delimiting them using semicolons.
6.3.1 Handle Cookies via Decorator Classes
Sun Microsystems' Smart Ticket blueprint v1.1 provides a class SessionConnector that utilizes the decorator pattern to add cookie support into the standard MIDP HTTP framework. The source code of this class is available from this book's Web site. The following snippet demonstrates how to use this class.
HttpConnection c = (HttpConnection) SessionConnector.open(url); // You can use "c" as a normal HttpConnection // but it is session aware now.
6.3.2 Handle Cookies via HttpClient Handlers
To support cookie headers in the HttpClient framework, we need to write the handler class. The source code of handler class (CookieHandler) is shown in Listing 6.4. Method getCookie() parses the response header and stores cookies in a static data member cookies. Method addCookie() matches stored cookies with the current request URL to determine which cookies to send out. Please refer to this book's Web site for complete source code.
Listing 6.4. The CookieHandler class
public class CookieHandler implements Handler {
private static Vector cookies;
private static Vector domains;
public CookieHandler() {
cookies = new Vector ();
domains = new Vector ();
}
public void prepareHeaders(HttpConnection c) throws Exception {
String url = c.getURL ();
addCookie(c, url);
}
public boolean processHeaders (HttpConnection c) throws Exception {
getCookie(c);
return false;
}
// Remove all cookies.
public void removeCookies() throws Exception {
cookies = new Vector ();
domains = new Vector ();
return;
}
// Retrieve cookies from the connection header
// and save them with domain information
private void getCookie(HttpConnection c) throws Exception {
// Parse the incoming cookies and store them in
// cookies and domains vectors.
}
private void addCookie(HttpConnection c,
String url) throws Exception {
// Match the url domain with existing cookies
// in the cookies vector. If a match is found,
// set it into the connection header.
}
}
The use of CookieHandler is illustrated in Listing 6.5.
Listing 6.5. The CookieHandler usage
HttpClient client = new HttpClient (); Handler h = new CookieHandler(); client.addHandler( h ); client.setUrl( url ); client.setRequestMethod( HttpConnection.GET ); byte [] result = client.query(null);6.4 HTTP Basic Authentication
Some HTTP headers can carry client credential information. Those credentials are used by servers to determine the client's identity and then grant or deny access to the requested resources. In the HTTP basic authentication scheme, the client sends its username and password in plain text with every request. The procedure is the following:
Use the Base64 algorithm to encode a username : password string
Send the encoded string and string Basic in the HTTP header Authorization
For example, if the username is Aladdin and password is open sesame, the HTTP authentication header is the following.
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==6.4.1 Code Example
To enable HTTP basic authentication in the HttpClient class, we need to plug in a handler (BasicAuthHandler). We can easily use BasicAuthHandler together with CookieHandler to make the HttpClient object keep track of a client session over an authentication connection (Listing 6.6).
Listing 6.6. Use cookies with HTTP basic authentication
HttpClient client = new HttpClient (); Handler h1 = new CookieHandler(); Handler h2 = new BasicAuthHandler(user, pass); client.addHandler( h1 ); client.addHandler( h2 ); client.setUrl( url ); client.setRequestMethod( HttpConnection.GET ); byte [] result = client.query(null);Sample source code for the BasicAuthHandler class is shown in Listing 6.7.
Listing 6.7. The BasicAuthHandler class
public class BasicAuthHandler implements Handler { private String username; private String password; public BasicAuthHandler (String u, String p) { username = u; password = p; } public void prepareHeaders(HttpConnection c) throws Exception { String s = encode(username + ":" + password); c.setRequestProperty("Authorization", "Basic " + s); } public boolean processHeaders(HttpConnection c) throws Exception { // Do nothing. return false; } // Base64 encoding. // // This implementation is adopted from // Kenneth Ballard's HttpClient package. // Released under LGPL. private String encode(String d) { // Implementation details skipped } }6.5 HTTP Digest Authentication
Basic authentication can be used in a secure network environment. However, in an insecure network, such as the Internet, the problem for basic authentication is obvious: A cracker can easily intercept the clear text username and password, and forge the user's identity. A more secure scheme is to use one-way hashes (digests) to carry user credentials. The HTTP Digest Authentication works as follows.
The client contacts the server and requests a restricted resource.
The server sends a challenge to the client, including a randomly generated nonce value in predefined HTTP headers.
The client calculates a hash using its username, password, and the nonce value according to an algorithm defined in the specification.
The client resends its request with the new authentication header.
The server compares hashes with its own calculations. If the authentication is successful, the client will continue to use the same hash until the server changes the nonce value or the user changes its username and password.
Besides eliminating the clear text username and password, the digest authentication scheme has other important benefits. The server knows only the hash of the password but not the password itself. This prevents insider abuses. The server nonce value is embedded in the hash and therefore cannot be forged. This allows the server to have better control over the authentication process.
6.5.1 Code Example
We can use the DigestAuthHandler class to make the HttpClient object aware of digest authentication. The implementation of DigestAuthHandler is based on Kenneth Ballard's Open Source package "HttpClient." Code snippet from the DigestAuthHandler class is shown in Listing 6.8. Please note that the processHeaders() method returns false when the authentication fails, causing the HttpClient to recalculate the digest headers and resubmit its request. The digest value is generated using code adopted from the Bouncy Castle package (see Chapter 20).
Listing 6.8. The DigestAuthHandler class
public class DigestAuthHandler implements Handler { public DigestAuthHandler (String u, String p) { username = u; password = p; } public void prepareHeaders(HttpConnection c) throws Exception { String h = "Digest "; if(username != null) h = h + "username=\"" + username + "\", "; if(realm != null) h = h + "realm=\"" + realm + "\", "; if(nonce != null) h = h + "nonce=\"" + nonce + "\", "; if(uri != null) h = h + "uri=\"" + uri + "\", "; if(opaque != null) h = h + "opaque=\"" + opaque + "\", "; if(qop != null) { h = h + "qop=\"" + qop + "\", "; // cnonce is a random number generated by the // client. You should use your device build-in // random number generator to produce it. cnonce = "0123456789"; h = h + "cnonce=\"" + cnonce + "\", "; h = h + "nc=" + count + ", "; // Increase counter by one. The counter will // be reset when a new nonce comes in. ncount++; String nc = Integer.toHexString(ncount); count = new String("00000000").substring(nc.length()) + nc; } h = h + "algorithm=\"MD5\", "; h = h + "response=\"" + getDigest() + "\""; c.setRequestProperty("Authorization", h); } public boolean processHeaders (HttpConnection c) throws Exception { if ( c.getResponseCode() == 401 ) { httpMethod = c.getRequestMethod(); uri = c.getFile(); parse (c.getHeaderField("WWW-Authenticate")); // need to re-send request return true; } else { return false; } } // Other utility methods }6.6 Secure HTTP
Both basic and digest HTTP authentication schemes discussed above are weak security measures. They only authenticate users but do not protect the
communication content. They do not prevent crackers from intercepting or even tampering with the communication data. For complete point-to-point HTTP
security, we need the HTTPS protocol that is based on secure underlying transport protocols such as the Secure Socket Layer (SSL) and the Transport Layer
Security (TLS). Compared with thin client solutions where security is provided by the fixed infrastructure, direct HTTPS connections allow more flexible security schemes. For example, the communication parties can decide what to encrypt, the level of encryption and how often the session key should be changed
based on their business needs. In addition, by eliminating the middleman, HTTPS smart clients avoid the single point of failure and hence they are not affected
by infrastructure level security holes. The discovery of security weaknesses in WAP gateways and WiFi access points has made this an important concern.
Figure 6.1 illustrates the difference between HTTPS end-to-end solutions and WAP thin client solutions.
6.6.1 HTTPS Support in the MIDP
Support for HTTPS is mandatory in the MIDP v2.0 but optional in the MIDP v1.0. To establish an HTTPS connection, all you need to do is pass
an https://-style URL string to the Connector.open() factory method.
- On an HTTPS-enabled MIDP v1.0 device, a normal HttpConnection object will be returned. You can open input and output streams as
usual. But the underlying data are properly encrypted. The entire process is transparent to developers.
On a MIDP v2.0 device, an HttpsConnection object will be returned. Interface HttpsConnection extends HttpConnection with two
more methods: getPort() and getSecurityInfo(). The getSecurityInfo() method returns a SecurityInfo object, which can be used to
obtain further information on cipher and server certificate.
