Mobile-enabling a web application without making application code changes

Many existing websites were designed without mobile devices in mind. To rebuild them for the mobile web could require a significant investment. This article demonstrates how you can provide a mobile interface to an existing web application without making any server-side code changes. This content is part of the IBM WebSphere Developer Technical Journal.

Christian Voth (cvoth@ca.ibm.com), Certified IT Architect, IBM

Christian Voth is a Certified IT Architect with IBM Software Services Group specializing in web technologies and enterprise Java. He holds a computer science degree from the University of Toronto and works out of the IBM Toronto Lab, Canada.



05 December 2012

Also available in Chinese Russian

Introduction

As mobile browsers become more capable with every release of Android, iOS, and other mobile operating systems, the physical constraints remain — namely the small screen size and the use of touch as the new input paradigm rather than a mouse. While some smartphones might be able to display a website, the display that results is often difficult to use at best, if not unusable altogether. Buttons and other controls need to be sized so that they can be easily manipulated by fingers rather than a mouse pointer, and new actions such as swipes and multi-finger gestures ought to be efficiently leveraged. In short, there needs to be a concerted effort made so that these sites can be not just mobile-friendly, but mobile-enabled.

Reduced screen real estate and touch controls are the primary factors that drive the need to simplify and re-organize the web UI so that a web application becomes usable on a mobile device. While it is possible to produce a single UI that renders differently on desktop and mobile browsers, this approach usually taken to avoid any modification of the existing web application. Instead, the goal should be to produce a set of static assets (markup, stylesheets, and JavaScript™ code) that provide mobile access to web app functionality without the need to modify the server-side code in any way. The existing site remains completely untouched.

From an operational standpoint, this avoids the often costly need to deploy code changes into a production environment. It also eliminates the need to regression test existing functionality that was modified during the course of adding mobile support. Also, because the same requests and responses are used, you only need to performance test the application if you expect adding mobile support to greatly increase your traffic. With this technique, simply drop in your static files (.html, .css, .js) so that they are served from the same domain hosting your existing web application, and reuse the existing services that your application already provides.

By limiting yourself to only creating new static assets, you are in effect only writing a client for your existing web app.

Web applications: A little history

Most non-trivial web applications were built around the notion of Model-View-Controller (MVC), where the controller resided on the server. The server maintained session state and, through forwards or redirects, navigated the user request to the next page to be rendered. Each client request typically resulted in markup for the entire page, even if only parts of that page changed from view to view. Portlet frameworks were introduced to solve this problem and enable more flexibility with page aggregation.

Then, Ajax exploded on the scene and Web 2.0 was born. Ajax enabled asynchronous requests to be made programmatically. The payload no longer had to be markup, but rather XML, and later JSON. This enabled the controller to be moved from server to client, and page aggregation could be offloaded to increasingly more capable and more standardized client browsers. At one extreme, you have a web application where the browser is little more than a markup renderer, with all function and flow managed by the server. On the other end of the spectrum, you have a web application where much of the control and flow logic executes on the client browser (as JavaScript, Flash, or Silverlight), with the server providing RESTful services to supply the data needs and any computational heavy-lifting the application might require.

When mobile web applications are mentioned, it’s this latter pattern that typically comes to mind, because this is how mobile frameworks such as jQuery Mobile or Dojo Mobile have been designed. But what happens when the application you need to mobile-enable has a more server-centric design without REST services?


The web application as black box

Since the intent is to avoid any and all code changes to the application, you can treat the application as a black box and simply focus on the output you get for any given input; input here refers to HTTP requests and output refers to HTTP responses. Therefore, if you create a mobile client that mimics a particular HTTP request and is able to parse and identify all the possible responses for that request, it becomes possible to supply a mobile “skin” that utilizes the same request/response patterns of the existing application. While this sounds simple enough in theory, in practice it might not be quite that easy.

Login sequence

Let’s look at an example of an authentication sequence for a typical web application. (The details about the HTTP request and response can be gathered using a tool such as Firefox Firebug or the Developer Tools in Chrome or Safari.) We'll start at authentication from the point at which the credentials are submitted. Listing 1 shows the source markup of the login form.

Listing 1
<form name="logonForm" action="/abc/salesportal/j_security_check" method="post">
<input type="hidden" name="page" value="signin.wss" />
<table cellspacing="0" cellpadding="0" class="sign-in-table">
   <tr>
   <td class="c1"><label for="user_id">Email ID:</label></td>
   <td><input dir="ltr" class="wpsEditField" size="30" 
         value="" name="j_username" id="user_id" type="text" /> 
	   (e.g., joe@abc.com)</td>
   </tr>
   <tr>
   <td class="c1"><label for="passwd">Password:</label></td>
   <td><input dir="ltr" class="wpsEditField" size="30" id="passwd" 
	   name="j_password" type="password" /></td>
   </tr>
   <tr><td><input type="hidden" name="login-form-type" value="pwd" /></td> </tr>
	   <tr>	<td>&nbsp;</td>
   <td><p><a href="http://www.abc.com/password">Forgot your password?
	   </a></p></td>
   </tr>
</table>

<table cellspacing="0" cellpadding="0" class="submit-table">
<tr>
   <td class="buttons" align="right">
   <span class="button-blue">
   <input type="submit" value="Submit" name="submitButton" />
   </span>
   <span class="button-blue">
   <input type="button" value="Cancel" name="cancelButton" 
	   onclick="cancelSignIn()" />
   </span>
   </td>
   </tr>
</table>
</form>

In the above code, when the user enters the user ID and password and clicks Submit, you can see that the credentials will be delivered to j_security_check. Simple enough. Let’s simulate an authentication request.

A very useful tool for blackbox web app testing is cURL, which enables you to construct and trace HTTP interactions (see Resources. It gives you full control, enabling you to construct every aspect of the request (Listing 2). Also, you never have to worry about the fickleness of browser caches.

Listing 2
curl --verbose --silent --show-error -d "page=signin.wss" --data-urlencode 
"j_username=-joe@abc.com" --data-urlencode "j_password=my+passw0rd" --data-urlencode 
"submit=Submit" http://www.abc.com/abc/salesportal/j_security_check

Notice that this cURL command ensures all user input fields have been URL encoded with --data-urlencode. This will also automatically set the content-type header to application/x-www-form-urlencoded. Also, the use of the --data* flag implies that this request will be a POST. There are a lot of flags with cURL and you will find you should be able to recreate any request you need.

Unfortunately, as you can see in Listing 3, this simple form POST doesn’t get you the response you expect.

Listing 3
* About to connect() to www.abc.com port 80 (#0)
*   Trying 192.168.1.1... connected
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> POST /abc/salesportal/j_security_check HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Accept: */*
> Content-Length: 91
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 302 Found
< Date: Fri, 18 Nov 2011 16:57:39 GMT
< Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47
< Location: http://www.abc.com/abc/salesportal/
< Expires: Thu, 01 Dec 1994 16:00:00 GMT
< Cache-Control: no-cache="set-cookie, set-cookie2"
< Content-Type: www/unknown
< Content-Language: en-US
< Content-Length: 0
< Proxy-Connection: Keep-Alive
< Connection: Keep-Alive
< Set-Cookie: JSESSIONID=0001KBumJR-0VZ6ybqAm1WIOn-N:15lgjrufm; Path=/
< Set-Cookie: LtpaToken2=g2VF5mHGQCmv73URFPz...zmsD5ifgdfSiJghQA/jJdUoABkaik=; 
	Path=/; Domain=.abc.com
< Set-Cookie: LtpaToken=3UpRmGDYVqd+1ZdKm03v...DSyZhJz4wpt+BFeBXTGn0Ww==; Path=/; 
	Domain=.abc.com
<
* Connection #0 to host www.abc.com left intact
* Closing connection #0

You appear to have been authenticated, due to the presence of the LTPA cookies, but your request got redirected to /abc/salesportal/.

By adding the --location flag to your cURL request, the redirects will be automatically followed. On the next attempt, you see the initial POST and 302 redirect (as above) as well as the subsequent request and response from the redirect in Listing 4.

Listing 4
* Connection #0 to host www.abc.com left intact
* Issue another request to this URL: 'http://www.abc.com/abc/salesportal/'
* Violate RFC 2616/10.3.3 and switch from POST to GET
* Re-using existing connection! (#0) with host www.abc.com
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> GET /abc/salesportal/ HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 18 Nov 2011 16:39:04 GMT
< Server: IBM_HTTP_Server/7.0.0.15
< Cache-Control: max-age=300
< Expires: Fri, 18 Nov 2011 16:44:04 GMT
< Content-Type: text/html;charset=ISO-8859-1
< Content-Length: 820
< Proxy-Connection: Keep-Alive
< Connection: Keep-Alive
< Age: 1545
<
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
 <head>
  <title>Index of /abc/salesportal</title>
 </head>
 <body>
<h1>Index of /abc/salesportal</h1>
<ul><li><a href="/abc/"> Parent Directory</a></li>
<li><a href="ccr/"> ccr/</a></li>
<li><a href="downloads/"> downloads/</a></li>
<li><a href="home2/"> home2/</a></li>
<li><a href="html/"> html/</a></li>
<li><a href="images/"> images/</a></li>
<li><a href="js/"> js/</a></li>
<li><a href="media/"> media/</a></li>
<li><a href="regs/"> regs/</a></li>
<li><a href="style/"> style/</a></li>
</ul>
</body></html>
* Connection #0 to host www.abc.com left intact
* Closing connection #0

Notice that while you appear to have been successfully authenticated, the resulting page is nothing more than a directory listing. Not what you were expecting. Time to look at the authentication flow in more detail. This time, we'll open up Chrome and use its Developer Tools to view the network details during a successful login (Figure 1).

Figure 1. Chrome Developer Tools
Figure 1. Chrome Developer Tools

Looking at the network details in Listing 5, you can see that the request posts the credentials, returns some cookies, and then redirects. If you overlook the request headers, you do so at your own peril. Not only is the form data being submitted (and there could be hidden fields), but cookies are as well.

The abcSurvey and UnicaNOIDID cookies look harmless enough, just a survey and site analytics. The JSESSIONID cookie refers to a server side session (Java™) which likely doesn’t contain anything of value that cannot be easily recreated, since you’ve just arrived at the web site and haven’t actually done anything. However, there is one cookie that stands out: WASReqURL.

Listing 5
Request URL: https://www.abc.com/abc/salesportal/j_security_check
Request Method:POST
Status Code:302 Found
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:105
Content-Type:application/x-www-form-urlencoded
Cookie:
abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; 
JSESSIONID=0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; 
WASReqURL=https:///abc/salesportal/signin.wss; 
Host:www.abc.com
Origin: https://www.abc.com
Referer: https://www.abc.com/abc/salesportal/registration/signIn.jsp
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 +
(KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2
Form Data
page:signin.wss
j_username:joe@abc.com
j_password: my+passw0rd
login-form-type:pwd
submitButton:Submit
Response Headers
Cache-Control:no-cache="set-cookie, set-cookie2"
Connection:Keep-Alive
Content-Language:en-US
Content-Length:0
Content-Type:www/unknown
Date:Thu, 17 Nov 2011 19:08:17 GMT
Expires:Thu, 01 Dec 1994 16:00:00 GMT
Location:https://www.abc.com/abc/salesportal/signin.wss
Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Set-Cookie:
WASReqURL=""; Expires=Thu, 01 Dec 1994 16:00:00 GMT; Path=/
LtpaToken2=jxSSeGottXC2WQZhQFeXf...PP1u+D/8PFtGPQs0sjiYanjdBJEfF0Zj6kXt1QeE=; 
Path=/
LtpaToken=bTzeMj+SSZFQMUKSZ20tc1...qPSY0kR5MFrKrqZDOwd7twfpupFNUpZUKlEGE41c=; 
Path=/

It appears that the presence of the WASReqURL cookie affects the redirect location.

The response then deletes the WASReqURL cookie by expiring it. It also sends along two new cookies: LtpaToken and LtpaToken2, both of which only get returned upon successful login. The Location header contains the URL to redirect to. That next request follows in Listing 6. Notice how it forwards the newly minted LTPA cookies in the request since WASReqURL has effectively been deleted. The web app now treats this request to /signin.wss as an authenticated request. This second request returns two new cookies: ODC_CG and ODC_REG.

Listing 6
Request URL:https://www.abc.com/abc/salesportal/signin.wss
Request Method:GET
Status Code:200 OK
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Cookie:
abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=
0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; 
LtpaToken2=jxSSeGottXC2WQZh...anjdBJEfF0Zj6kXt1QeE=; 
LtpaToken=bTzeMj+SSZFQMUKSZ...twfpupFNUpZUKlEGE41c=
Host:www.abc.com
Referer:https://www.abc.com/abc/salesportal/registration/signIn.jsp
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 
(KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2

Response Headers
Cache-Control:no-cache="set-cookie, set-cookie2"
Connection:Keep-Alive
Content-Encoding:gzip
Content-Language:en-US
Content-Type:text/html;charset=ISO-8859-1
Date:Thu, 17 Nov 2011 19:08:20 GMT
Expires:Thu, 01 Dec 1994 16:00:00 GMT
Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Set-Cookie:
ODC_CG=123456; Domain=abc.com
ODC_REG=Joe Park|joe@abc.com; Expires=Fri, 16 Nov 2012 19:08:19 GMT; 
Path=/; Domain=abc.com
Transfer-Encoding:chunked

The payload of this second request includes all the markup to render the authenticated home page for the web app (not shown).

Test the sequence

Before you begin writing any code, it’s a good idea to create a standalone request to validate your understanding of the request/response flow. With the insight provided by the network trace in Chrome’s developer tools, you can construct a new cURL login request (Listing 7).

Listing 7
curl --verbose --silent --show-error -d "page=signin.wss" --data-urlencode 
"j_username=-joe@abc.com" --data-urlencode "j_password=my+passw0rd" --data-urlencode 
"submit=Submit" --cookie "WASReqURL=http:///abc/salesportal/signin.wss" --cookie-jar 
login-cookies.txt http://www.abc.com/abc/salesportal/j_security_check

Without saving the cookies, cURL didn’t behave properly. Once it received the Set-Cookie header for WASReqURL, indicating that the cookie had expired, cURL seemed to ignore that and continued to forward the cookie (unchanged) in the subsequent redirected request. To fix this, enable cookie persistence with --cookie-jar. Once enabled, any cookie changes in a 302 are passed to the next request.

Notice that the WASReqURL cookie has been added to the request. It is also necessary to have cURL record the cookies (--cookie-jar, see sidebar) since the WASReqURL is expired within a redirect.

cURL output is shown in Listing 8.

Listing 8
* About to connect() to www.abc.com port 80 (#0)
*   Trying 192.168.1.1... connected
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> POST /abc/salesportal/j_security_check HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Cookie: WASReqURL=http:///abc/salesportal/signin.wss
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
> Content-Length: 91
> Content-Type: application/x-www-form-urlencoded

The POST request is made with the WASReqURL cookie (Listing 9).

Listing 9
< HTTP/1.1 302 Found
< Date: Fri, 18 Nov 2011 19:40:56 GMT
< Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47
< Location: http://www.abc.com/abc/salesportal/signin.wss
< Expires: Thu, 01 Dec 1994 16:00:00 GMT
< Cache-Control: no-cache="set-cookie, set-cookie2"
< Content-Type: www/unknown
< Content-Language: en-US
< Content-Length: 0
< Proxy-Connection: Keep-Alive
< Connection: Keep-Alive
* Added cookie JSESSIONID="0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm" for domain 
	www.abc.com, path /, expire 0
< Set-Cookie: JSESSIONID=0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm; Path=/
* Added cookie WASReqURL="""" for domain www.abc.com, path /, expire 786297600
< Set-Cookie: WASReqURL=""; Expires=Thu, 01 Dec 1994 16:00:00 GMT; Path=/
* Added cookie LtpaToken2="gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=" 
	for domain abc.com, path /, expire 0
< Set-Cookie: LtpaToken2=gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=; 
	Path=/; Domain=.abc.com
* Added cookie LtpaToken="3UpRmGDYVqd+1Zd788+5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg/
	deBXTGn0Ww==" for domain abc.com, path /, expire 0
< Set-Cookie: LtpaToken=3UpRmGDYVqd+1Zd788+5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg
	/deBXTGn0Ww==; Path=/; Domain=.abc.com

You get a 302 redirect to /signin.wss. The WASReqURL cookie is expired, and because your credentials are good, you get two LTPA cookies (Listing 10).

Listing 10
* Connection #0 to host www.abc.com left intact
* Issue another request to this URL: 'http://www.abc.com/abc/salesportal/signin.wss'
* Violate RFC 2616/10.3.3 and switch from POST to GET
* Re-using existing connection! (#0) with host www.abc.com
* Connected to www.abc.com (192.168.1.1) port 80 (#0)
> GET /abc/salesportal/signin.wss HTTP/1.1
> User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5
> Host: www.abc.com
> Cookie: LtpaToken2=gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=; 
JSESSIONID=0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm; LtpaToken=3UpRmGDYVqd+1Zd788+
5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg/deBXTGn0Ww==; WASReqURL=http:///abc/salesportal/
signin.wss
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

The redirect URL (/signin.wss) is followed and the LTPA and JSESSIONID cookies are forwarded. Notice here that cURL erroneously indicates that the WASReqURL cookie is forwarded, despite the fact that it was expired in the previous response. However, because you have enabled cookie persistence in cURL (--cookie-jar), the cookie was not sent, regardless of what this says.

The contents of the cookie-jar file is shown in Listing 11.

Listing 11
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This file was generated by libcurl! Edit at your own risk.

www.abc.com FALSE /     FALSE  0          JSESSIONID  0001iuu...RRZLDYy4B:15lgjrufm
www.abc.com FALSE /     FALSE  786297600  WASReqURL   ""
.abc.com    TRUE  /     FALSE  0          LtpaToken2  gsHvV8D1CBzy5...I0RUTpn2UnanTo0=
.abc.com    TRUE  /     FALSE  0          LtpaToken   3s1Zd88+5x...hJz4wpt+BXTGn0Ww==
.abc.com    TRUE  /abc/salesportal/ FALSE	0  ODC_CG   ""
.abc.com    TRUE  /     FALSE  1353181257 ODC_REG     Joe Smith|joe@abc.com

The GET for /signin.wss completes successfully. It returns two more cookies: ODC_CG and ODC_REG (Listing 12).

Listing 12
> HTTP/1.1 200 OK
> Date: Fri, 18 Nov 2011 19:40:57 GMT
> Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47
> Expires: Thu, 01 Dec 1994 16:00:00 GMT
> Cache-Control: no-cache="set-cookie, set-cookie2"
> Content-Type: text/html;charset=ISO-8859-1
> Content-Language: en-US
> Transfer-Encoding: chunked
> Proxy-Connection: Keep-Alive
> Connection: Keep-Alive
* Added cookie ODC_CG="""" for domain abc.com, path /abc/salesportal/, expire 0
> Set-Cookie: ODC_CG=""; Domain=abc.com
* Added cookie ODC_REG="Joe Smith|joe@abc.com" for domain abc.com, path /, expire 
1353181257
> Set-Cookie: ODC_REG=Joe Smith|joe@abc.com; Expires=Sat, 17 Nov 2012 19:40:57 
GMT; Path=/; Domain=abc.com
>
* Connection #0 to host www.abc.com left intact
* Closing connection #0

The markup that came back in that final response (not shown) was for the authenticated home page, so you know all your inputs (as well as the proper forwarding of expired cookies) resulted in a successful authentication.

Login sequence summary

From this entire sequence, you have determined the authentication inputs (Table 1) and, upon successful login, the outputs (Table 2).

Table 1
Input typeDetail
CookieabcSurvey=1321469743354; <<< UNIMPORTANT
UnicaNIODID=h8DDq7LCdW1-XQbeXFj; <<< UNIMPORTANT
JSESSIONID=0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; <<< UNIMPORTANT
WASReqURL=https:///abc/salesportal/signin.wss;
Formpage:signin.wss <<< HIDDEN
j_username:joe@abc.com
j_password: my+passw0rd
login-form-type:pwd <<< HIDDEN
submitButton:Submit <<< UNIMPORTANT
Form data is encoded asContent-Type:application/x-www-form-urlencoded
Table 2
Output type Detail
CookiesWASReqURL=https:///abc/salesportal/signin.wss; <<< CREATED then DESTROYED
LtpaToken2=jxSSeGottXC2WQZhQFeXfGHapYGLTzXDDvLf...cl8PFtGPQs0sjiYanjdBJEfF0Zj6kXt1QeE=; Path=/
LtpaToken=bTzeMj+SSZFQMUKSZ20tc1efTrTZodEkEkqPt...nR5MFrKrqZDOwd7twfpupFNUpZUKlEGE41c=; Path=/
ODC_CG=123456; Domain=abc.com
ODC_REG=Joe Smith|joe@abc.com; Expires=Fri, 16 Nov 2012 19:08:19 GMT; Path=/; Domain=abc.com

The markup for the authenticated home page is shown in Listing 13.

Listing 13
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en-US">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
<meta name="description" content="Registered Home Page Salesportal" />
<meta name="keywords" scheme="iso8601" 
content="ABC, Salesportal, sellers, home, registered, signed in" /> ...

So, in order to create a mobile login page, you need to be able to send the same inputs. The form inputs will be no problem, but the cookie, namely WASReqURL, will pose a challenge.

Your mobile login page will be part of a new static file you create — the mobile “skin” mentioned earlier. Let’s call it mobile.html. It will be served from the same server as the existing app, so the Single Origin security Policy (SOP) will not be a hindrance. When the user submits a mobile authentication request, it will be sent by XMLHttpRequest (XHR) which, as you know, is every bit an HTTP request, but done behind the scenes. These XHR or Ajax requests, however, are not identical to your typical HTTP request. Restrictions apply, namely the ability to simply set the “Cookie” header in your programmatic XHR request (as you did with the cURL utility). According to the W3 specification for XMLHttpRequest, this particular header is restricted.

Also, creating the WASReqURL cookie client-side will not cause it to be included in the next response back to the server because the cookie domain can only be set server-side. The only way to include this cookie in a request is to receive it from the server in the first place. Therefore, you need to know when the WASReqURL cookie was created. The request for the login page is shown in Listing 14.

Listing 14
Request URL:https://www.abc.com/abc/salesportal/signin.wss
Request Method:GET
Status Code: 302 Found
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Cookie: abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=
0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945
Host: www.abc.com
Referer: https://www.abc.com/abc/salesportal/home.wss
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, 
like Gecko) Chrome/15.0.874.106 Safari/535.2

Response Headers
Cache-Control: no-cache="set-cookie, set-cookie2"
Connection: Keep-Alive
Content-Language: en-US
Content-Length: 0
Content-Type: www/unknown
Date: Thu, 17 Nov 2011 18:50:02 GMT
Expires: Thu, 01 Dec 1994 16:00:00 GMT
Location: https://www.abc.com/abc/salesportal/registration/signIn.jsp
Server: IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Set-Cookie: WASReqURL=https:///abc/salesportal/signin.wss; Path=/

Notice that this initial request for the signin.wss page gets redirected to /registration/signIn.jsp. More importantly, notice that a cookie WASReqURL is sent back in the response. This cookie gets sent along in the second request (Listing 15). (A bug in desktop Safari 5.1.1 for Windows prevents cookies that are created in a 302 response from being forwarded in the subsequent GET request. This works in Safari for iOS however.)

Listing 15
Request URL:https://www.abc.com/abc/salesportal/registration/signIn.jsp
Request Method:GET
Status Code:200 OK
Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Cookie:abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=
0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; WASReqURL=https:///abc/salesportal/
signin.wss
Host:www.abc.com
Referer:https://www.abc.com/abc/salesportal/home.wss
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, 
like Gecko) Chrome/15.0.874.106 Safari/535.2
Response Headers
Connection:Keep-Alive
Content-Encoding:gzip
Content-Language:en-US
Content-Type:text/html;charset=ISO-8859-1
Date:Thu, 17 Nov 2011 18:50:03 GMT
Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47
Transfer-Encoding:chunked

Recreating the login sequence with Ajax

Based on this investigation, your mobile client will need to make a GET request for /signin.wss for the sole purpose of receiving the WASReqURL cookie before the login form is submitted.

Using jQuery, that request would look like Listing 16.

Listing 16
$.ajax({
	type: 'GET',
	url: '/abc/salesportal/signin.wss',	
	async: false,
	complete: function(xhr, textStatus) {
		 if(xhr.readyState == 4) 
		 {
			 if(xhr.status == 200) { console.log(document.cookie); }
		 }
	}
});

In the JavaScript console you should see WASReqURL=http:///abc/salesportal/signin.wss (among other cookies).

Once the WASReqURL cookie is present on the client, you can create the POST request used to send the login credentials to the server.

Once the user submitted their credentials, you’ll recall the POST to /abc/salesportal/j_security_check in the captured sequence resulted in a 302 redirect to /abc/salesportal/signin.wss. HTTP redirects also result in a method change, in this case from POST to GET. Any cookies returned in the first request will be forwarded in the second request (assuming it is allowed by the cookie Path). All of this occurs transparently within the Ajax request. Programmatically, you will not be able to see the 302 redirect nor the intermediate response and (re)request (Listing 17).

Listing 17
var params = 'j_username=' + encodeURIComponent($('#user_id').val()) + '&j_password=' 
+ encodeURIComponent($('#passwd').val()) + '&page=signin.wss&submit=Submit';
		
$.ajax({
	type: 'POST',
	url:'/abc/salesportal/j_security_check',	
	contentType: 'application/x-www-form-urlencoded',
	data: params,				
	cache: false,
// Cannot set the Cookie header - restricted		
//	headers:{'Cookie':'WASReqURL=http:///abc/salesportal/signin.wss'},
// Another attempt to set the Cookie header:
//	beforeSend: function(xhr){ xhr.setRequestHeader( "Cookie", 
"WASReqURL=http:///abc/salesportal/signin.wss"); },
		complete: function(xhr, textStatus) {  …}
});

Remember to encode user-supplied form-based input using encodeURIComponent() when you are POSTing data with the application/x-www-form-urlencoded content type.

Once you send your authentication request, you need to know whether it was successful or not.

The problem in both cases is that the request will be (transparently) redirected to a new page. With a RESTful approach, you would simply get back a response code of 2xx for success or 401 for unauthorized. Alternatively, you might get back a custom response code packed into a JSON payload. However, for this particular web application, these convenient responses are not available, so we have to make do with the current request-response pattern:

  • Successful logins will be redirected to /abc/salesportal/signin.wss
  • Unsuccessful logins will be redirected to /abc/salesportal/registration/signInError.jsp
  • In both cases the final HTTP status code will be 200 (OK).

The redirect transparency of Ajax requests is problematic because the client will not receive any notification of redirects or what the ultimate URL is (the Location header for the 302 redirect is lost).

The XHR JavaScript object does not make available the request, which means the ultimate request URL is not known. All that is available to determine how the request was handled is to analyse the response headers (on the last request) or the response payload (on the last request).

In the Listing 17, this is done in the complete() function.

To simplify parsing of the response markup, convert the response to XML (if it isn’t already), as shown in Listing 18. (See Resources for a more robust way to accomplish this if you are concerned with more than just WebKit-based browsers.)

Listing 18
complete: function(xhr, textStatus) {
if(xhr.readyState == 4) 
{
	if(xhr.status == 200)
	{
		var xmlResponse = xhr.responseXML;
		if(xhr.responseXML == null)
      		 {
			if(typeof DOMParser != "undefined") 
			{ 	                     
				var parser = new DOMParser();
				xmlResponse = parser.parseFromString(xhr.responseText,
				"text/xml");
			}
		}	
	}
	/*
	  else if(xhr.status == 302 || xhr.status == 303) {} // will never see this, 
only the 200 for the signInError page
	  else if(xhr.status == 401) // UNAUTHORIZED - will never see this as 
application redirects to signInError page
	*/
}}

Once you have the parsed response DOM, the next step is to uniquely identify data on the signin.wss and signInError.jsp pages that is unique to that page. It needs to be always present and, ideally, something that does not change when the page undergoes a UI facelift. Metatags make a prime candidate. The keywords tag is used here because the success page contained “signed in” and the error page contained “failure” and “error” keywords (Listing 19).

Listing 19
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/
xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en-US">
<head>
<meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
<meta name="description" content="Registered Home Page Salesportal" />
<meta name="keywords" scheme="iso8601" content="ABC, Salesportal, sellers, home, 
registered, signed in" /> ...

Using the response DOM retrieved above, jQuery selectors prove to be very useful for extracting the content from the keyword metatag (Listing 20).

Listing 20
var isErrorPage = true;
var metaKeywordsTag = $('meta[name="keywords"]',xmlResponse);

if(metaKeywordsTag != null)
{
	var keywords = metaKeywordsTag.attr("content");

	if(keywords != null && ((keywords.indexOf("fail") >= 0) || (keywords.indexOf
		("error") >= 0)))
	{ isErrorPage = true; }
	else
	{
		if(keywords != null && (keywords.indexOf("signed in") >= 0))
			isErrorPage = false;
	}                		                		
}

Now that you have the isErrorPage flag set, your mobile client can display the next page in the authenticated page flow or the login error dialog.


Conclusion

From the login example described in this article, you can see how it is possible to recreate the same request-response sequence expected by a web application programmatically using XmlHTTPRequest. By treating the application as a black box, faithfully recreating the inputs and using a robust technique to interpret the output, it is possible to avoid making changes to the existing application in order to provide mobile client support. Because there are no server code changes, there is no need to undergo a potentially expensive deployment into a production environment, nor is there a need for extensive regression and performance testing.

Ideally, a website should be architected from the ground up with Responsive Web Design in mind. However, the reality is that there are many websites that were designed without mobile devices in mind, and a significant investment would be required to rebuild them for the mobile web. This technique offers a compromise for getting your website on the mobile web sooner.

Resources

Learn

Get products and technologies

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into WebSphere on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=WebSphere, Mobile development
ArticleID=848588
ArticleTitle=Mobile-enabling a web application without making application code changes
publish-date=12052012