Showing posts with label ServletFilters. Show all posts
Showing posts with label ServletFilters. Show all posts

Monday, March 23, 2009

GZip Compress Static Files

My PeopleSoft web server contains several JavaScript and CSS files that I embed in PeopleSoft pages using the various techniques described in this blog. Unlike files served by the PeopleSoft application, the web server does not GZip compress these static text files. Until recently, I used the GZip ServletFilter described by Jayson Falkner in his post Two Servlet Filters Every Web Application Should Have. But GZipping every request for the same static file seemed like an unnecessary waste of CPU cycles. I got to thinking...

Could I eliminate the CPU cycles expended by GZipping those static files on every request? Is there a way store static GZipped content and serve the GZipped version to browsers that accept GZip encoding while still making the plain text version available to other browsers?

Here is the solution I cooked up: using the following rules with the tuckey.org UrlRewriteFilter ServletFilter, I can serve a static GZip file to browsers that accept GZip compression while still serving the plain text version to browsers that don't.

The URL Rewrite rules:

<!-- Browsers that support GZip -->
<rule>
<condition type="header" name="Accept-Encoding">.*gzip.*</condition>
<from>^/scripts/([^&lt;&gt;:"/\|?*]+\.js)$</from>
<to type="forward">/compressed/bin/scripts/$1.gz</to>
<set type="response-header" name="Content-Encoding">gzip</set>
</rule>

<rule>
<condition type="header" name="Accept-Encoding">.*gzip.*</condition>
<from>^/css/([^&lt;&gt;:"/\|?*]+\.css)$</from>
<to type="forward">/compressed/bin/css/$1.gz</to>
<set type="response-header" name="Content-Encoding">gzip</set>
</rule>

<!-- Browsers that do NOT support GZip -->
<rule>
<condition type="header" name="Accept-Encoding" operator="notequal">.*gzip.*</condition>
<from>^/scripts/([^&lt;&gt;:"/\|?*]+\.js)$</from>
<to type="forward">/compressed/minified/scripts/$1</to>
</rule>

<rule>
<condition type="header" name="Accept-Encoding" operator="notequal">.*gzip.*</condition>
<from>^/css/([^&lt;&gt;:"/\|?*]+\.css)$</from>
<to type="forward">/compressed/minified/css/$1</to>
</rule>

From this rules file, you can see that I store GZip compressed versions of my JavaScript files in /compressed/bin/scripts. If a requesting browser has the value gzip in the Accept-Encoding header and the request is for a file in the /scripts/ directory, then the first rule tells the UrlRewriteFilter to add the response header Content-Encoding: gzip and serve the version located at /compressed/bin/scripts/. Rule #2 is similar, but for CSS files. Rules 3 and 4 are the inverse of rules 1 and 2, telling the UrlRewriteFilter to serve files from /compressed/minified/scripts/ and /compressed/minified/css/. For example, if an IE 7 browser requests /scripts/jquery-1.3.2.min.js, then the UrlRewriteFilter will add the Content-Encoding: gzip response header and serve /compressed/bin/scripts/jquery-1.3.2.min.js.gz. If an LWP Perl browser posted the same request, then the UrlRewriteFilter would serve the file located at /compressed/minified/scripts/jquery-1.3.2.min.js (assuming the LWP Accept-Encoding header is not set).

Using URL Rewriting in this manner, I don't serve files from my /scripts/ and /css/ directories, and, therefore, don't need these directories on my web server. To avoid confusion caused by developers searching for these files on my web server, I put readme.txt files in each of these directories to explain that URL's pointing at these directories are rewritten to the /compressed directory.

If you use the UrlRewriteFilter, then be sure to use filter-mapping patterns that won't rewrite PeopleSoft URL's. Here are my mappings for the /css/ and /scripts/ URL's

<filter-mapping>
<filter-name>GzipFilter</filter-name>
<url-pattern>/scripts/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>GzipFilter</filter-name>
<url-pattern>/css/*</url-pattern>
</filter-mapping>

You can create GZip versions of your files using the following command:

cat $file_location/scripts/jquery.js | gzip --stdout -9 > $file_location/bin/scripts/jquery.js.gz

And on Windows:

cat %file_location%\scripts\jquery.js | gzip --stdout -9 > %file_location%\bin\scripts\jquery.js.gz

Unfortunately, Windows doesn't have a cat or gzip command. To utilize these commands on Windows, I recommend installing UnxUtils.

Caveat: adding ServletFilters as described in this post may violate your PeopleSoft limited use web server license. Consult your license agreement to ensure compliance. If you use WebLogic, you can leverage the full power of your WebLogic instance by purchasing a WebLogic license from your Oracle rep. For a low cost alternative, you can reverse proxy your PeopleSoft web server with Apache's httpd server and use Apache's URL Rewrite engine (mod_rewrite) instead of the ServletFilter mentioned in this post.

Monday, November 12, 2007

Desktop Integrated Signon

Several months ago I had the opportunity to configure a PeopleSoft system to "trust" users' desktop credentials. Some would call this single signon or even Desktop Integrated Signon. Implementing desktop integrated signon requires some configuration and a small amount of development. The process looks like this:

  1. Download the JCIFS NtlmHttpFilter,

  2. Modify the filter to pass the desktop user name to the app server as a request header (compile and deploy included, of course),

  3. Write some signon PeopleCode,

  4. Enable public access,

  5. Enable signon PeopleCode, and

  6. Configure your web server to use your new filter (see the JCIFS NtlmHttpFilter documentation for configuration details as details will differ depending on your environment).

After downloading the filter and filter source code, open the NtlmHttpServletRequest and implement the getHeader and getHeaderNames overrides with the following code.

    public String getHeader(String name) {

if(name.equals("XX_REMOTE_USER") {
return getRemoteUser();
} else {
HttpServletRequest req = (HttpServletRequest)this.getRequest();
return req.getHeader(name);
}

}
public Enumeration getHeaderNames() {
Vector headers = new Vector();
HttpServletRequest req = (HttpServletRequest)this.getRequest();

for (Enumeration e = req.getHeaderNames() ; e.hasMoreElements() ;) {
headers.add(e.nextElement());
}

headers.add("XX_REMOTE_USER");
return headers.elements();
}

Next, put the following PeopleCode in a FUNCLIB:

Function WWW_NTLM_AUTHENTICATE()
Local string &userName = %Request.GetHeader("XX_REMOTE_USER");
Local number &foundSlash = Find("/", &userName);

REM ** remove the NT/AD domain;
If(&foundSlash > 0) Then
&userName = Substring(&userName, &foundSlash + 1, Len(&userName));
Else
&foundSlash = Find("\", &username);
If(&foundSlash > 0) Then
&userName = Substring(&userName, &foundSlash + 1, Len(&userName));
Else
End-If;

If(Len(&userName) > 0) Then
SetAuthenticationResult(True, &userName);
Else
SetAuthenticationResult(False, &userName, "Web server authentication failure");
End-If;
End-Function;

Like I said, it has been a few months since I wrote this code, and, unfortunately, I'm typing it here from memory. Please correct any mistakes I've made. Refer to PeopleBooks for enabling signon PeopleCode and enabling public access. Both are documented in the Security Administration PeopleBook.

A couple of issues I've found with this approach:

  • If you are using Enterprise Portal and want to allow desktop integrated signon to both the portal and to the content provider apps, then you will need to further customize the filter to skip NTLM on your content provider web servers when the client is the portal server. Otherwise, the NtlmHttpFilter will not allow portal to access those web servers (this only affects homepage creation when you have pagelets that come from a content provider). If you only access your PeopleSoft systems through Enterprise Portal, then this is not an issue. Likewise, if you are configuring this solution on your PeopleSoft applications and you do not have Enterprise Portal, then this is not an issue.

  • In this scenario, your app server trusts the security information provided by the web server, bypassing the app server's standard authentication routine. This may pose a security threat if users can gain access to your app server. To mitigate this risk, you may want to either hide your app server behind a firewall or perform additional validation/authentication (digitally encrypt the user ID request header on the web server with a certificate and decrypt it on the app server, pass the NTLM authentication token on to the app server and validate it again, etc).

  • Since this solution requires your web server to trust your desktop, make sure your organization has a strong password policy forcing strong passwords. If you use a desktop integrated sign on solution, then any user that can gain access to a desktop by cracking a password can also gain access to your Enterprise applications. As an alternative to passwords, consider key fobs.

If you would like to allow administrators to log in as someone other than their desktop user (psadmin, for example), then you can add an "if" test to your signon PeopleCode that compares %SignonUserId to the public user name. If the user name is the same as your public user name, then log the user into the application as the user given by the web server. Otherwise, return from this function and allow the standard signon processing to authenticate the user.