Since my employer Sun Microsystems informed me that I was being made redundant last month I've been on so-called "gardening leave", and for the first time in a very long time I've had time to do stuff just for the hell of it. I decided I'd take a look at NetBeans, a IDE for Java that I'd heard good things about.
After grabbing the latest JDK, at Gary's suggestion I downloaded a copy of the Open Source NetBeans IDE to have a play. I've never been a particular fan of IDEs, but NetBeans is actually very good. As well as the editor and debugger there's also GUI designer and a load of other bits, and it all works together very well. I particularly like the 'as you go' syntax checking which highlights errors in your code in much the same way as the auto spellchecker works in a word processor - the erroneous code is underlined in red, and if you move the cursor over the line you get a diagnostic message. The editor also support folding, something I first saw years ago in the Occam editor.
The next job was to think of something small but useful to write. Although I've already deployed some anti-blogspam measures on this site, I'm beginning to notice a gradual increase in attacks - inevitably the spammers are getting wise to the more common tricks used to put them off. Some countermeasures such as the "answer this maths question" approach used by blogs.sun.com are trivially circumventable. The most popular and sucessful countermeasure at the moment seems to be to use a captcha, but personally I don't like them as I feel they are intrusive, and despite the hype about them the implementations often have flaws that still leave them open to attack. The problem is that HTTP is a stateless protocol, so each page has to contain enough context to enable the server to verify that the response to the captcha is correct, whether that be a hidden form field, a cookie or whatever. Because of that, any such scheme is vunerable to capture/replay attacks. Even using HTTPS to encrypt the communication channel doesn't protect against the attacker viewing the page and/or cookie source and figuring out the protection mechanism.
I therefore decided that obfustication of the communication between the webserver and browser was probably a reasonable approach, and one way of doing this was to implement comment submission using a Java applet. However MovableType uses HTML forms and HTTP POSTs requests to submit comments, and as I didn't want to rewrite the back-end I had to figure out how to get a Java applet to behave as if it were a HTML form.
The first step was to figure out how to obfusticate the form contents so that it wouldn't be possible to figure out by inspecting the HTML which form fields were required to submit a comment. To do this the CGI script on the server needed to send out the form with one set of field names, and the browser needed to submit the form back to the server with a different set of names. In addition I wanted to change the names from the default MovableType names and hide the comment submission URL so that existing attack scripts wouldn't work. To do this I embeded a non-visible form into the comment submission page - I made it invisible by making all the individual fields hidden:
<form id="CommentForm" name="CommentForm" method="" action="">
<input type="hidden" name="entry_id" value="9999" />
<input type="hidden" name="input1" value="" />
<input type="hidden" name="input2" value="" />
<input type="hidden" name="input3" value="" />
<input type="hidden" name="input4" value="" />
<input type="hidden" name="submit1" value="" />
</form>
Notice also that the action attribute of the form is empty so you can't tell from the HTML the URL of the comment submission script. It is filled in by the applet when the submit button is pressed, along with the names and values of the hidden fields. The next bit of the jigsaw was to write the applet to display the form - a snip using the NetBeans GUI builder - and to figure out how to access the HTML of the containing page from the applet, which was a little harder. It turns out there are two possible ways to do this. The first is by calling back from Java into Javascript to obtain information from the browser (Netscape called this mechanism LiveConnect, it's also known as JSObject). Documentation for the API can be found in the Java plugin developer's guide. The second method us by direct DOM access via the Common DOM API. However the JSObject mechanism is older and appears to have better browser support - a bit of googling revealed that the Common DOM API was pretty flaky, and rather than providing a get/set interface like the JSObject API it requires you to access the DOM using a callback mechanism using a subclass of DOMAction - yuck. Needless to say, I used the JSObject mechanism. In order to use it it's necessary to enable it by adding the mayscript attribute to the applet used to load up the applet:
<applet id="CommentApplet" class="CommentApplet"
codebase="/blog/java/CommentApplet.jar"
code="CommentForm.class" width=350 height=350 mayscript="true">
</applet>
In order to use the JSObject methods to manipulate the form's fields it's first necesary to get handles to the appropriate entity within the enclosing HTML document:
JApplet myApplet;
:
JSObject obj = JSObject.getWindow(MyApplet);
JSObject doc = (JSObject) obj.getMember("document");
Object args = new Object[1];
args[0] = "CommentForm";
JSObject form = (JSObject) doc.call("getElementById", args);
Having done that it is easy to manipulate the form, for example here's how the method and action attributes are set:
String method, action;
:
form.setMember("method", method);
form.setMember("action", action);
Sending the form back to the webserver requires that we call the JavaScript submit method on the form, like this:
Object[] args = new Object[0];
form.call("submit", args);
I made the corresponding changes to the MovableType lib/MT/App/Comments.pm module to map the form field names to/from the obfusticated versions, changed the MovableType comments templates to contain the new form and applet tag definitions and everything worked as I expected. However the applet looked significantly different from the rest of the comment page - the font and foreground/background colours were all different from the rest of the page, and the buttons looked like the standard Java ones instead of normal HTML form ones. A little googling solved the problem - Java has pluggable "look and feel", although it's poorly documented. The relevant documentation is the UIManager and PLAF classes. It's possible to tell Java to use the platform L&F like this:
javax.swing.UIManager.setLookAndFeel(
javax.swing.UIManager.getSystemLookAndFeelClassName());
which makes the buttons and any dialogs adopt the platform L&F rather than the standard Java one. The next thing was to see if it was possible to make the applet stylesheet-aware as well, so that it picked the fonts and colours up from the enclosing page. My first thought was to grab the stylesheet entry associated with the block that the applet was in, but it became apparent that approach wouldn't work - style attributes are inherited, so the actual attributes used to render a block are the merge of the explicit style attributes specified for the block, plus any inherited from the parent blocks. What we actually need to do is to get the calculated attributes. Unfortunately, cross-browser portability rears it's ugly head at this point - Mozilla/Firefox and Internet Explorer have different mechanisms to do this, Firefox uses a call to getComputedStyle() to get the style object, which can then be queried with getPropertyValue() whereas IE uses an attribute of the object called currentStyle to hold the style information. In addition, the format of the attribute names is different in the two browsers - Firefox expects normal CSS hyphenated attribute names, e.g. background-color whilst IE requires them to be camelCased, e.g. backgroundColor. There are other differences as well - Firefox returns colours as a string of the form rgb(10,20,30) whilst IE returns them as a colour name, e.g. red or as a HTML colour code, e.g. #102030.
The last wrinkle is to do with font sizes, which are quite frankly a complete mess when you are trying to get predictable cross-platform behaviour. There are whole slew of issues:
- Internet Explorer returns a relative font size if that's how the font size was specified in the stylesheet, e.g.
medium instead of a pixel or point size, so it's impossible to know what point size this actually corresponds to.
- In Firefox when you query the parent document style object for the font size you get a value back in pixels. Java however expects font sizes to be specified in points, so you would expect to have to scale the pixel value by the display resolution.
- Windows doesn't use the real DPI value of the display, it uses one of two fixed sizes, 96 DPI (the "small fonts" display setting) or 120 DPI (the "large fonts" option). Querying the display resolution form Java (using
Toolkit.getScreenResolution()) therefore returns a fake value.
- There are a number of long-standing bugs/quirks in the font size handling in Java pre-JRE 1.5 - basically Java assumes that all displays have a pixel pitch of 72 DPI, irrespective of the actual resolution.
The easiest way of dealing with this mess was to encapsulate the differences in two classes:
// Interface for CSS readers.
private interface CssReader {
String getAttr(String name);
int parseFontSize(String size);
Color parseColor(String color);
}
// Class to read Mozilla CSS.
private class MozillaCssReader implements CssReader {
public MozillaCssReader(JSObject doc, JSObject applet) {
JSObject obj = (JSObject) doc.getMember("defaultView");
Object[] args = new Object[2];
args[0] = applet;
args[1] = new String("");
style = (JSObject) obj.call("getComputedStyle", args);
fontSizeRE = Pattern.compile("\\A([\\d.]+)(p[tx])\\z",
Pattern.CASE_INSENSITIVE);
rgbRE = Pattern.compile(
"\\Argb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)\\z",
Pattern.CASE_INSENSITIVE);
}
public String getAttr(String name) {
Object[] args = new Object[1];
args[0] = name;
return((String) style.call("getPropertyValue", args));
}
public int parseFontSize(String size) {
System.out.println(size);
Matcher m = fontSizeRE.matcher(size);
if (m.matches()) {
// Java assumes the display is 72 DPI,
// so "px" and "pt" sizes are equivalent.
return((int) (Float.parseFloat(m.group(1)) + 0.5F));
} else {
return(defaultFontSize);
}
}
public Color parseColor(String color) {
if (color.equals("transparent")) {
return(new Color(0xFF, 0xFF, 0xFF, 1));
} else {
Matcher m = rgbRE.matcher(color);
m.matches();
return(new Color(Integer.parseInt(m.group(1)),
Integer.parseInt(m.group(2)),
Integer.parseInt(m.group(3))));
}
}
private JSObject style;
private Pattern fontSizeRE;
private Pattern rgbRE;
}
// Class to read Explorer CSS.
private class ExplorerCssReader implements CssReader {
ExplorerCssReader(JSObject doc, JSObject applet) {
style = (JSObject) applet.getMember("currentStyle");
attrRE = Pattern.compile("-[a-z]");
sizeToPoint = new HashMap();
sizeToPoint.put("xx-small", new Integer(6));
sizeToPoint.put("x-small", new Integer(8));
sizeToPoint.put("small", new Integer(10));
sizeToPoint.put("medium", new Integer(12));
sizeToPoint.put("large", new Integer(14));
sizeToPoint.put("x-large", new Integer(18));
sizeToPoint.put("xx-large", new Integer(24));
ss = new StyleSheet();
}
public String getAttr(String name) {
Matcher m = attrRE.matcher(name);
StringBuffer nm = new StringBuffer();
while (m.find()) {
String r = m.group().substring(1).toUpperCase();
m.appendReplacement(nm, r);
}
m.appendTail(nm);
return((String) style.getMember(nm.toString()));
}
public int parseFontSize(String size) {
Object sz;
if ((sz = sizeToPoint.get(size)) != null) {
return(((Integer) sz).intValue());
} else {
return(defaultFontSize);
}
}
public Color parseColor(String color) {
if (color.equals("transparent")) {
return(new Color(0xFF, 0xFF, 0xFF, 1.0F));
} else {
return(ss.stringToColor(color));
}
}
The last step is to figure out which browser the applet is running in. As IE doesn't have the getPropertyValue() method, the easiest way is to try to fetch it and trap the resulting exception we get when running under IE:
CssReader css = null;
try {
form.getMember("getPropertyValue");
css = new MozillaCssReader(doc, applet);
} catch (JSException e) {
css = new ExplorerCssReader(doc, applet);
}
Phew. If you want to see it all in action, follow the "Comments" link below. It's not perfect as I've had to work around several misfeatures as described above, but it does work, at least with Firefox and IE. I've had a report that it doesn't work in Safari - if any Safari users out there can send me the Java Console stack trace they get when it fails, I'll try to fix it!