Reviving Yahoo! PageBuilder in 2020

Anybody who used GeoCities in the early 2000s probably remembers using PageBuilder, the strange Java drag-and-drop interface that you would launch from your browser. I wanted to try it out again, but GeoCities is long gone. That's not stopping me, though...

Tracking down PageBuilder

GeoCities homepage in 2003 on the Wayback Machine, trumpeting the ease of use and features offered by GeoCities Free

Usually, the Wayback Machine does a pretty good job at archiving websites. It's great for static content, but anything with server-side interactivity falls flat. You can look at the GeoCities marketing material all you want, and it'll even show you an archived copy of the Yahoo! login form circa 2003. That's where it stops, though.

This means we can't experience the wonders of janky pre-XMLHttpRequest file management interfaces. So, we're going to be SOL for a lot of GeoCities. PageBuilder was mostly client-side though, so perhaps it's been cached somewhere...?

I did some searching and came across a couple of strange domains which just contained GeoCities new-account template pages from back in 2003. There's a direct link to PageBuilder on this page: http://geocities.yahoo.com/v/pb.html


Promotional blurb from the PageBuilder page in 2006, with a handy warning that it can take up to 3-5 minutes to load on a 28k modem

I plugged that URL into the Wayback Machine. Picking any of the mid-2000s archived copies gets you a beautiful page describing PageBuilder itself. Unfortunately, we're once again taunted by technological bitrot: "You must be logged in to use PageBuilder!"

This page also has a bunch of quick-start links to launch PageBuilder with different preset templates. Alas, all of these just call a JavaScript function that redirects you to the login page.

My next thought was... this is a Java application, it's probably stored in a .jar file. Perhaps somebody else has already archived it. I did a Google search for yahoo pagebuilder jar, and it's mostly junk, but the second results page links to a file on GitHub with stats about unbound links from GeoCities pages. This clued me into the existence of the pagebuilder.yahoo.com domain.

It's a little clunky to get to, but the Wayback Machine allows you to list every URL it knows about on a given domain by using wildcards. I tried this on my newly found PageBuilder domain, and got a very interesting set of 6,171 results.

Sorting the results by MIME type returns archived copies of some very promising-looking URLs - these are just a small subset:

Here it looks like we've found the PageBuilder client! There were a bunch of different versions archived, so I grabbed the latest one the Wayback Machine had, which was 2.61.67.

It should be easy to get it to run - after all, it's Java. Write Once, Run Anywhere. We still have JVMs today.

$ java -jar client.jar
Error: Invalid or corrupt jarfile client.jar

Oh, of course. That would have been too simple.

Reviving a Java Applet

PageBuilder isn't a standalone Java application, it's actually an applet. Applet support is kind of sparse in browsers these days, and I was on the sofa with my laptop (running macOS 10.14) and didn't want to faff about with installing different JREs and JDKs and browser plugins in the vague hopes something might work.

I went straight for JADX (my usual go-to decompiler for Java and Android stuff) and threw in client.jar. It's not obfuscated at all, and it's pretty simple.

Everything happens inside y.WDDesignerA, which extends java.applet.Applet. I decided to try writing a tiny shell class that exposed a standard Java main method and created an instance of the applet.

According to the docs, the browser calls init() to tell the applet it's been loaded and then calls start() to tell the applet to start. That seemed pretty straightforward, so I just did that.

It seemed to try and initialise, but then failed several levels deep inside JDK code when it tried to create a UI element. After some debugging I found out why: an applet expected to be part of an existing container with a top-level window, and I wasn't doing that. I created an AWT Frame and added the WDDesignerA applet to it, and that fixed it.

import y.WDDesignerA;
import java.awt.*;

class RunStandalone {
    public static void main(String[] args) {
        Frame frame = new Frame();
        frame.setSize(100, 100);
        frame.setTitle("Holder");
        frame.setVisible(true);
        WDDesignerA applet = new WDDesignerA();
        frame.add(applet);
        applet.init();
        applet.start();
    }
}

The next challenge was resources.

$ javac -cp lib/client.jar RunStandalone.java
$ java -cp lib/client.jar:. RunStandalone
----- startup -----
Exception in thread "main" java.lang.NullPointerException
        at java.desktop/java.applet.Applet.getParameter(Applet.java:205)
        at y.WDDesignerA.init(WDDesignerA.java:148)
        at RunStandalone.main(RunStandalone.java:130)

Thanks to the decompilation, I had a decent idea about what was going on inside WDDesignerA.init(). Here's the method in question, so that you can follow:

public final void init() {
    instance = this;
    System.out.println("----- startup -----");
    setLayout(new BorderLayout(0, 0));
    addNotify();
    this.uid = getParameter("PARAM1");
    this.devURL = getParameter("PARAM2");
    hostBase = getCodeBase();
    try {
        if (!hostBase.getProtocol().startsWith("file") || this.uid == null || this.devURL == null) {
            this.uid = null;
            String parameter = getParameter("SERVERROOT");
            if (parameter == null) {
                parameter = "../../../server/";
            }
            hostBase = new URL(hostBase, parameter);
        } else {
            hostBase = new URL(this.devURL);
        }
        System.out.println(new StringBuffer("hostBase: ").append(hostBase).toString());
        String parameter2 = getParameter("SPLASHIMG");
        if (parameter2 == null) {
            parameter2 = "pbsplash.gif";
        }
        Image appletImage = Util.getAppletImage(parameter2);
        String parameter3 = getParameter("MSGFILE");
        if (parameter3 == null) {
            parameter3 = "ini/messages.properties";
        }
        Util.loadMessages(parameter3);
        this.mainButton = new Button(Util.msg(370));
        add("Center", this.mainButton);
        this.initAlert = new InitAlert(new Dimension(300, 250), Color.white, Color.blue, appletImage, 21);
        this.initAlert.show();
        initPoint(Util.msg(371));
        this.startTimeStamp = System.currentTimeMillis();
        if (getParameter("INIFILE") == null) {
        }
        checkpoint("a.id");
        this.f25t = new Thread(this);
        this.f25t.start();
    } catch (Exception e) {
        checkpoint("wdda:loginfail:");
        e.printStackTrace();
        goBodyPage(LOGINFAIL_PAGE, Util.msg(372));
        logException(e);
        if (this.initAlert != null) {
            this.initAlert.dispose();
            this.initAlert = null;
        }
    }
    WDDesignerA.super.init();
}

It fetches two parameters called PARAM1 and PARAM2, and stores these into this.uid and this.devURL respectively. Then, it works out a hostBase and finally tries to load some bits and pieces.

The next step was to implement the AppletStub interface in my skeleton code and call the applet's setStub() method. This allowed me to supply values for parameters and for the codebase (in a real browser, these would come from the HTML that creates the applet), so I was able to poke around and try to make it work.

I decided to supply a file:/ URL to a file inside the current directory as the codebase, so that PageBuilder would load its resources from local files. I also supplied a blank SERVERROOT parameter so that it wouldn't use the default of "../../../server/". With this, it would now try to load some files like images/pbsplash.gif and ini/messages.properties - progress!

I obtained these from the Wayback Machine and placed them in, as well as various other image files that PageBuilder asked for afterwards. Now the loading screen would appear, but the console would get spammed by errors like this...

y.io.FilestoreException: 52 - (url:file:/home/ash/src/pagebuilder/x/write/logs/error); java.net.UnknownServiceException: protocol doesn't support output
        at y.io.OldRemoteFilestore.doPost(OldRemoteFilestore.java:242)
        at y.io.OldRemoteFilestore.write(OldRemoteFilestore.java:353)
        at y.io.ServerFile.write(ServerFile.java:463)
        at y.io.ServerFile.write(ServerFile.java:449)
        at y.io.WriteRunnable.run(ServerFileManager.java:113)
        at y.io.WrappedRunnable.run(ServerFileManager.java:141)
        at y.io.WorkerThread.run(ServerFileManager.java:169)

Clearly, my current approach wasn't going to work - PageBuilder was expecting to talk to some sort of filestore API which no longer exists.

Implementing the Filestore

Everything in PageBuilder's y.io package deals with file management on 'filestores', which seem to be a generic abstraction over filesystems. These support a bunch of pretty normal operations: copy, delete, list, mkdir, mklink, move, read, set_attrib and write.

There's a few different sorts implemented: a read-only URLFilestore, the fancy OldRemoteFilestore (I don't know what happened to the new one), a CompositeFilestore which merges together multiple filestores into one space, and the FilterFilestore and PageUpgradeFilestore that don't seem all too interesting.

FilestoreFactory instantiates a bunch of these, which gives us a better idea of what's going on:

private static void init() {
    System.out.println("FilestoreFactory.init");
    ServerFile.initStatics();
    filestores = new Hashtable();
    String url = WDDesignerA.getHostBase().toString();
    if (url.endsWith("/")) {
        url = url.substring(0, url.length() - 1);
    }
    filestores.put("<default>", new OldRemoteFilestore("<default>", url, "/user"));
    filestores.put("<default>/metafiles", new OldRemoteFilestore("<default>/metafiles", url, "/user/.geobuilder"));
    OldRemoteFilestore oldRemoteFilestore = new OldRemoteFilestore("<default>/clipart", url, "/clipart");
    oldRemoteFilestore.setURLPrefix(Preferences.get(Preferences.KEY_CLIPART_ROOT));
    filestores.put("<default>/clipart", oldRemoteFilestore);
    filestores.put("<default>/templates", new OldRemoteFilestore("<default>/templates", url, "/templates"));
    filestores.put("<default>/addons", new OldRemoteFilestore("<default>/addons", url, "/objects"));
    filestores.put("<default>/old/pages", new OldRemoteFilestore("<default>/old/pages", url, "/user/.pages"));
    filestores.put("<default>/pages", new PageUpgradeFilestore("<default>/pages", "<default>/old/pages"));
    filestores.put("<default>/old/userini", new OldRemoteFilestore("<default>/old/userini", url, "/ini/user.ini"));
    filestores.put("<default>/logs", new OldRemoteFilestore("<default>/logs", url, "/logs"));
    CompositeFilestore compositeFilestore = new CompositeFilestore("clipart");
    try {
        ServerFile serverFile = new ServerFile("@<default>:/");
        compositeFilestore.mount("/c.gif", new ServerFile("@<default>/clipart:/c.gif"));
        compositeFilestore.mount("/pictures/Clipart", new ServerFile("@<default>/clipart:/pictures"));
        compositeFilestore.mount("/pictures/User Files", serverFile);
        compositeFilestore.mount("/buttons/Buttons", new ServerFile("@<default>/clipart:/buttons"));
        compositeFilestore.mount("/buttons/User Files", serverFile);
        compositeFilestore.mount("/bullets/Bullets", new ServerFile("@<default>/clipart:/bullets"));
        compositeFilestore.mount("/bullets/User Files", serverFile);
        compositeFilestore.mount("/horizontalLines/Horizontal Lines", new ServerFile("@<default>/clipart:/hrules"));
        compositeFilestore.mount("/horizontalLines/User Files", serverFile);
        compositeFilestore.mount("/verticalLines/Vertical Lines", new ServerFile("@<default>/clipart:/vrules"));
        compositeFilestore.mount("/verticalLines/User Files", serverFile);
        compositeFilestore.mount("/backgrounds/Backgrounds", new ServerFile("@<default>/clipart:/backgrounds"));
        compositeFilestore.mount("/backgrounds/User Files", serverFile);
        compositeFilestore.mount("/proxies/Clipart", new ServerFile("@<default>/clipart:/proxies/Pictures"));
        compositeFilestore.mount("/proxies/User Files", serverFile);
    } catch (FilestoreException e) {
        e.printStackTrace();
    }
    filestores.put("clipart", compositeFilestore);
    filestores.put("url", new URLFilestore("url", new StringBuffer(String.valueOf(url)).append("/read/url/").toString()));
}

These are seemingly used to read a lot of stuff. I'll need to implement this if I actually want to make PageBuilder work.

I was getting tons of errors about a failure to POST to /write/logs/error, so I decided to target that first. I put together a very simple Node.js script that would listen on port 9797 (a very arbitrary choice) and print out whatever it received under /write/, and return a 404 for anything else.

I had to make PageBuilder talk to it as well, so I decided to trigger the this.devURL codepath in WDDesignerA.init() by supplying an arbitrary string (uid - it doesn't seem to matter what this is) as PARAM1, and the URL to my slapdash API server as PARAM2.

Suddenly, I had a lot of really interesting output. PageBuilder still wouldn't work, but just trying to invoke it once got me all this output:

GET :: /read/clipart/proxies/Pictures/bgmusic.gif
GET :: /read/clipart/proxies/Pictures/bg.gif
GET :: /read/clipart/proxies/Pictures/bgmusic.gif
GET :: /read/clipart/proxies/Pictures/bg.gif
GET :: /read/clipart/proxies/Pictures/bg.gif
GET :: /read/clipart/proxies/Pictures/bgmusic.gif
GET :: /read/clipart/proxies/Pictures/bg.gif
GET :: /read/clipart/proxies/Pictures/bgmusic.gif
GET :: /read/clipart/proxies/Pictures/bg.gif
GET :: /read/clipart/proxies/Pictures/bgmusic.gif
POST :: /write/logs/error
Geocities       Yahoo! PageBuilder 2.61.67 exception:
y.io.FilestoreException: 52 - (url:http://localhost:9797/read/clipart/proxies/Pictures/bgmusic.gif); java.io.FileNotFoundException: http://localhost:9797/read/clipart/proxies/Pictures/bgmusic.gif
        at y.io.OldRemoteFilestore.doGet(OldRemoteFilestore.java:212)
        at y.io.OldRemoteFilestore.read(OldRemoteFilestore.java:326)
        at y.io.OldRemoteFilestore.readImage(OldRemoteFilestore.java:333)
        at y.io.ServerFile.getImage(ServerFile.java:403)
        at y.io.CompositeFilestore.readImage(CompositeFilestore.java:63)
        at y.io.ServerFile.getImage(ServerFile.java:403)
        at y.io.ReadImageRunnable.run(ServerFileManager.java:73)
        at y.io.WrappedRunnable.run(ServerFileManager.java:141)
        at y.io.WorkerThread.run(ServerFileManager.java:169)

POST :: /write/logs/error
Geocities       Yahoo! PageBuilder 2.61.67 exception:
y.io.FilestoreException: 52 - (url:http://localhost:9797/read/clipart/proxies/Pictures/bg.gif); java.io.FileNotFoundException: http://localhost:9797/read/clipart/proxies/Pictures/bg.gif
        at y.io.OldRemoteFilestore.doGet(OldRemoteFilestore.java:212)
        at y.io.OldRemoteFilestore.read(OldRemoteFilestore.java:326)
        at y.io.OldRemoteFilestore.readImage(OldRemoteFilestore.java:333)
        at y.io.ServerFile.getImage(ServerFile.java:403)
        at y.io.CompositeFilestore.readImage(CompositeFilestore.java:63)
        at y.io.ServerFile.getImage(ServerFile.java:403)
        at y.io.ReadImageRunnable.run(ServerFileManager.java:73)
        at y.io.WrappedRunnable.run(ServerFileManager.java:141)
        at y.io.WorkerThread.run(ServerFileManager.java:169)

GET :: /read/user/.geobuilder/user.ini
GET :: /read/user/.geobuilder/user.ini
GET :: /read/user/.geobuilder/user.ini
GET :: /read/user/.geobuilder/user.ini
GET :: /read/user/.geobuilder/user.ini
POST :: /write/logs/error
Geocities       Yahoo! PageBuilder 2.61.67 exception:
y.io.FilestoreException: 52 - (url:http://localhost:9797/read/user/.geobuilder/user.ini); java.io.FileNotFoundException: http://localhost:9797/read/user/.geobuilder/user.ini
        at y.io.OldRemoteFilestore.doGet(OldRemoteFilestore.java:212)
        at y.io.OldRemoteFilestore.read(OldRemoteFilestore.java:326)
        at y.io.ServerFile.getInputStream(ServerFile.java:360)
        at y.Preferences.loadUserProperties(Preferences.java:165)
        at y.Preferences.login(Preferences.java:118)
        at y.Designer.login(Designer.java:441)
        at y.Designer.<init>(Designer.java:406)
        at y.WDDesignerA.run(WDDesignerA.java:676)
        at java.base/java.lang.Thread.run(Thread.java:834)

GET :: /read/ini/user.ini
GET :: /read/ini/user.ini
GET :: /read/ini/user.ini
GET :: /read/ini/user.ini
GET :: /read/ini/user.ini
POST :: /write/logs/error
Geocities       Yahoo! PageBuilder 2.61.67 exception:
y.io.FilestoreException: 52 - (url:http://localhost:9797/read/ini/user.ini); java.io.FileNotFoundException: http://localhost:9797/read/ini/user.ini
        at y.io.OldRemoteFilestore.doGet(OldRemoteFilestore.java:212)
        at y.io.OldRemoteFilestore.getUserIni(OldRemoteFilestore.java:273)
        at y.io.OldRemoteFilestore.init(OldRemoteFilestore.java:282)
        at y.io.ServerFile.init(ServerFile.java:685)
        at y.Preferences.loadLoginProperties(Preferences.java:187)
        at y.Preferences.login(Preferences.java:119)
        at y.Designer.login(Designer.java:441)
        at y.Designer.<init>(Designer.java:406)
        at y.WDDesignerA.run(WDDesignerA.java:676)
        at java.base/java.lang.Thread.run(Thread.java:834)

GET :: /read/templates/Blank.page
GET :: /read/templates/Blank.page
GET :: /read/templates/Blank.page
GET :: /read/templates/Blank.page
GET :: /read/templates/Blank.page
POST :: /write/logs/error
Geocities       Yahoo! PageBuilder 2.61.67 exception:
y.io.FilestoreException: 52 - (url:http://localhost:9797/read/templates/Blank.page); java.io.FileNotFoundException: http://localhost:9797/read/templates/Blank.page
        at y.io.OldRemoteFilestore.doGet(OldRemoteFilestore.java:212)
        at y.io.OldRemoteFilestore.read(OldRemoteFilestore.java:326)
        at y.io.ServerFile.getInputStream(ServerFile.java:360)
        at y.io.PageHandler.readPage(PageHandler.java:16)
        at y.Designer.login(Designer.java:463)
        at y.Designer.<init>(Designer.java:406)
        at y.WDDesignerA.run(WDDesignerA.java:676)
        at java.base/java.lang.Thread.run(Thread.java:834)

POST :: /write/logs/error
Geocities       Yahoo! PageBuilder 2.61.67 exception:
java.lang.InternalError: required key 'Key(MEMBERNAME, class java.lang.String)' is not defined anywhere (global.ini, defaults, etc.)
        at y.Preferences.getRaw(Preferences.java:213)
        at y.Preferences.get(Preferences.java:226)
        at y.Preferences.get(Preferences.java:220)
        at y.Designer.login(Designer.java:509)
        at y.Designer.<init>(Designer.java:406)
        at y.WDDesignerA.run(WDDesignerA.java:676)
        at java.base/java.lang.Thread.run(Thread.java:834)

I knew it was trying to read some GIFs, a couple of user-specific INI files, and a blank page template. I went back to the Wayback Machine to look for these.

I was able to grab bgmusic.gif but not bg.gif (I guess it never got captured by whatever process found these URLs originally), so I just edited the former and put a big question mark over it to serve as a placeholder file. I got the blank page as well.

The user-specific INI files proved to be a little more difficult; the Wayback Machine couldn't capture these, of course. Not for lack of trying:

--YPB Error:401:com.yahoo.publishing.io.FilestoreException: 32 - Remote error: user's identity could not be verified - invalid Y cookie
com.yahoo.publishing.util.HttpException: 401 com.yahoo.publishing.io.FilestoreException: 32 - Remote error: user's identity could not be verified - invalid Y cookie
    at com.yahoo.publishing.pagebuilder.server.AbstractFileRequestHandler.handleRequest(AbstractFileRequestHandler.java:311)
    at com.yahoo.publishing.pagebuilder.server.PagebuilderServlet.service(PagebuilderServlet.java:138)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:588)
    at com.livesoftware.jrun.JRun.runServlet(JRun.java)
    at com.livesoftware.jrun.JRunGeneric.handleConnection(JRunGeneric.java)
    at com.livesoftware.jrun.JRunGeneric.handleProxyConnection(JRunGeneric.java)
    at com.livesoftware.jrun.service.proxy.JRunProxyServiceHandler.handleRequest(JRunProxyServiceHandler.java)
    at com.livesoftware.jrun.service.ThreadConfigHandler.run(ThreadConfigHandler.java)

I would have to figure out what to put in them myself. It looked like it was expecting to see MEMBERNAME and URLPREFIX keys, so I put those into both of those files.

That was the last missing piece to get PageBuilder to boot up and show me a glorious AWT interface.

PageBuilder running under KDE on Linux in 2020, with extremely bad font rendering and Motif-esque widget styles

It sort of works. You can't add most of the Add-Ons or add pictures because they expect to find more files on the filestore. The Wayback Machine has archived a substantial amount of these, but some are missing, and some (like obj-guestbook2.page) throw odd exceptions I didn't investigate.

There's also an issue I couldn't figure out, where sometimes the whole UI will just get stuck and stop processing events.

I tried to figure out what was going on, and made a horrifying realisation. Not only is PageBuilder built using AWT, it's also using an old AWT event model which was deprecated in Java 1.1 - almost 24 years ago.

I decided that I would try a contemporary Java virtual machine, running inside a contemporary web browser. I put together a skeleton HTML page that would load the applet (bypassing my shoddy launcher class entirely) and booted up Mac OS 9.2.1 in QEMU, where I had Netscape 4 installed.

PageBuilder running inside Netscape 4 in Mac OS 9, displaying the list of Yahooligans widgets for your webpage

The result is... broken in different ways. It doesn't seem to hang like it does on 2020 Linux or on macOS 10.14. On the other hand, the colour picker and the dialog for setting up IE page effects (which both work on Java 11) do not work on it.

It'd be interesting to see how it behaves on Windows with a slightly newer JVM - that might be the sweet spot, given that this version of PageBuilder dates back to June 2002.

What next?

I've already sunk more time into this silly project than I wanted to, so I'm stopping here for now. PageBuilder runs, albeit you can't really do anything useful with it - most notably because my simple API server for the Filestore doesn't implement operations like list and write.

You can download the results here: https://wuffs.org/files/pagebuilder-2.61.67-proof-of-concept.zip

Alongside the various PageBuilder resources I collected from the Wayback Machine, this package includes:

I'd love to find newer versions of PageBuilder as well, but I'm not sure how to go about that - I'm sure Yahoo! didn't just stop updating it in June 2002. Either the newer versions weren't captured by the Wayback Machine, or they moved to a different domain I don't know about.

Who knows what else the future could bring though - 2020 is full of surprises :p


Previous Post: Researching the Digitime Tech FOTA Backdoors
Next Post: Experimenting with AI Dungeon