Dieser Beitrag zeigt, wie man den Code einer auf WebSphere laufenden proprietären Java-Anwendung analysieren kann. Der Anwendungsfall ist hierfür die Communitys-Anwendung von HCL Connections, mit dem Ziel, mehr über die Funktionsweise der Atom-API zu erfahren.
Ausgangslage: Eine fehlerhafte Dokumentation
Mein Vorhaben war recht simpel: Eine Community automatisiert per API anlegen, damit dies in Drittanbieter-Software eingebunden werden konnte. In der Dokumentation findet sich dazu ein Artikel inklusive Beispiel des Atom-Dokumentes. Beschrieben sind die Felder author und contributor dort nicht, allerdings enthält dieser Artikel mehr Informationen über contributor. Jedoch wird per API lediglich die Community mit dem Nutzer als Eigentümer angelegt, der sich für die API-Anfrage authentifiziert. Eine ganze Reihe an abgeänderten Tests ändert daran nichts und führt auch zu keinen Fehlern, die möglicherweise weitere Hinweise liefern. Es scheint, als würden diese beiden Felder an dieser Stelle schlichtweg ignoriert.
Da es sich um proprietäre Software handelt, kann man leider nicht mal eben einen Blick in den Quellcode werfen und ggf. sogar mit Debugging weitere Informationen ermitteln. Bei Java mit seinem Bytecode haben wir jedoch den großen Vorteil, dass sich dieser recht leicht dekompilieren lässt. Dies hat zwar auch seine Nachteile. So können wir den Code nicht einfach verändern und es gehen Informationen wie etwa die Bezeichnungen von Variablen verloren. Doch es bleibt die einzige Möglichkeit, mehr über die Funktion zu erfahren. Immerhin erhält man dadurch wieder Java-Code und muss nicht mit Assembler hantieren, was bei einem Projekt dieser Größe zu einer sinnvollen praktikablen Fehleranalyse völlig unrealistisch sein dürfte.
Möglicherweise muss der Code aber auch nicht verändert werden – sondern es genügt bereits ein Java-Namensraum, über den sich das Trace-Logging in WebSphere aktivieren lässt. Bei den bisherigen Versuchen meldet sich eine Klasse namens MemberProfile im SystemOut.log des InfraClusters zu Wort, jedoch ohne Namensraum. Diesen können wir anhand des Java-Codes ermitteln.
Export der EAR-Datei
Zunächst müssen wir die Ear-Datei herunterladen. Ein sogenanntes Enterprise Archive ist im Grunde ein ZIP-Archiv mit den kompilierten Class-Dateien sowie weiteren Metadaten zur Java-Anwendung. Auch JSPs sind dabei. Da diese nicht kompiliert werden, kann man sie ohne Decompiler analysieren und mit jedem Texteditor verändern. Interessant ist hier vor allem die Ear-Datei der Communitys-Anwendung – darüber laufen auch die Atom-APIs. Man kann sie in der WebSphere ISC unter Applications > Application Types > WebSphere enterprise applications anhaken und per Export herunterladen:
Analyse mit dem Java Decompiler JD-GUI
Die Ear-Datei könnte man mit jedem beliebigen Archivprogramm entpacken. Das ist jedoch wenig zielführend: Sie enthält Großteils weitere War- und Jar-Dateien der einzelnen Komponenten. Auch das sind gepackte Java-Anwendungen. Ein Teil der Inhalte sind direkt lesbar, etwa die JSPs. Das Layout ist jedoch wenig hilfreich, wir benötigen den Bytecode in den Class-Dateien.
Statt das Archiv zu entpacken, wird ein Programm zum dekopilieren benötigt. Es macht den Bytecode wieder lesbar. Ein solches quelloffenes Tool ist der Java Decompiler. Man kann ihn in die Entwicklungsumgebung Eclipse integrieren, oder in Form von JD-GUI als eigenständige Anwendung nutzen – wir werden von letzterem Gebrauch machen. Die Software hat eine lange Geschichte. Bereits zu U-Hacks Zeiten kam sie zur Analyse des Knuddels-Applets zum Einsatz. Ihr könnt entweder die fertigen Pakete/Binaries herunterladen, oder die portable Jar-Datei. Hierfür ist lediglich Java in mindestens Version 1.8 nötig:
java -jar ~/Downloads/jd-gui-1.6.6.jar
Das zuvor heruntergeladene Communities.ear Archiv per Drag & Drop hinein ziehen. Im linken Bereich werden alle Inhalte sichtbar. Es ist von Vorteil, sich mit der grundlegenden Struktur von Java-Anwendungen bereits auszukennen. Ansonsten wird man etwas länger benötigen, um diese herauszufinden.
Wo ist die API?
Spätestens wenn man bei der Anwendungslogik angelangt ist, wird der Umfang klar. Schließlich ist darin nicht nur die API enthalten – sondern auch der Code für die Communitys selbst. Eine erste Orientierung bietet application.xml im META-INF Ordner. Sie enthält eine Zuordnung der Pfad-Präfixe zu den jeweiligen Modulen. Die weiter unten definierten Komponenten wie etwa die Anwendungsrollen aus der ISC sind an dieser Stelle wenig hilfreich. Interessant: Alles im Präfix /communities wird zu comm.web.war geleitet. Ausgenommen sind nur zwei Unterpfade, die mit der API wenig zu tun haben. Daher schaue ich mir comm.web.war genauer an.
In WEB-INF/jsps/atom/error sind wir der API schon recht nahe: Dort liegt die Fehlerseite, die bei fehlerhaften Anfragen den Statuscode und einen Infotext anzeigt. Durch eine Testanpassung mit einer bewussten fehlerhaften XML-Anfrage lässt sich das bestätigen:
<error xmlns="http://www.ibm.com/xmlns/prod/sn">
<danielWasHere>true</danielWasHere>
<code>400</code>
<message>
Invalid member entry.
</message>
<trace>
</trace>
</error>
Wir sind also auf dem richtigen Weg. Ansonsten befindet sich im directory Ordner von jsps eine Reihe an Templates, welche die XML-Antworten generieren: community.jsp beispielsweise für eine einzige Community. Darin sind auch Filter enthalten. So wird beispielsweise die E-Mail Adresse für Externe ausgeblendet, damit diese sich per API keine Liste aller Mitarbeiter bzw. sogar externer (Konkurrenz-) Partner generieren können. Das html Verzeichnis enthält ebenfalls JSPs, allerdings für das Web. Etwa die Startseite mit den verschiedenen Layouts. Interessant, aber all diese Ansichten helfen uns nicht weiter – wir brauchen die Anwendungslogik.
In dieser Hinsicht befindet sich nichts relevantes in diesem Modul. Daher habe ich den Unterordner lib angeschaut – vermutlich wurde Layout und Logik getrennt, was üblich und sinnvoll ist. Mehrere mit comm.* startende Jar-Dateien fallen auf, da comm offensichtlich als Abkürzung für Communitys verwendet wurde. Nur com wäre logischer, steht allerdings ohne Präfix in Konflikt mit dem commercial Namensraum.
Jedenfalls klingt comm.api.jar zielführend. In com.ibm gefolgt von lconn.comm und data finden sich verschiedene Models/DTOs, etwa CommunityValues – allerdings ohne Logik. Sonst gibt es nur tango, der wenig vielversprechend klingt, aber andererseits auch die Frage aufwirft, was da wohl drin liegt. Einiges wie sich zeigt, config enthält mit dem ConfigManager jene Klasse, um die Anwendungsspezifische XML-Datei im LotusConnections-config Ordner zu parsen. Thematisch passender wirkt Community in data, dies scheint das Model einer Community zu sein. Richtig interessant wird es im Ordner service: In der unscheinbaren Klasse TangoService liegt eine Schnittstelle, die Methoden wie createCommunity oder updateCommunity enthält:
Übrigens ist AntiVirusFilter in util für das senden von Dateien an einen Virenscanner zuständig. Eine Klasse die man nicht unbedingt hier erwartet, da die Konfiguration dafür in der LCC liegt und nicht in Communitys.
Was die API angeht, sehe ich keine Implementierung und schaue daher weiter in comm.web.jar, in dem sich mit tango.web ein bekannter Namensraum findet. Hier gibt es ein Unterpaket atom mit einer Klasse AtomParser. Parst dieser eine Community als Atom-Feed oder anders herum? Es handelt sich um letzteres, also genau die Richtung, welche wir bei einer schreibenden Anfrage benötigen:
Die Methode convertEntry2Community wird hierbei aufgerufen. Sie erhält ein Entry Objekt – eine Schnittstelle, die Getter- und Setter einer per API erhaltene Community beschreibt. Dies liegt allerdings in einem ganz anderen Namensraum, der org.apache.abdera.model heißt. Dass wir dort richtig sind, lässt sich übrigens ebenfalls verifizieren: Zu Beginn wird geprüft, ob der Community-Eintrag (<entry>) gültig ist. Falls nicht, wird eine Exception mit dem Bezeichner atom.parser.invalidCommunityEntryType geworfen. Den Text könnt ihr in den Properties nachlesen. Um ihn zu provozieren, genügt es, statt <entry> beispielsweise <entry1> zu senden. Dann erhalten wir besagten Text Invalid Community Entry. Die Exception wird in AtomExceptionHandler abgefangen, für andere Fehler setzt dieser Handler den Schlüssel aus den Properties sowie den Statuscode.
Was haben wir gelernt?
Schaut man sich die komplette Methode genauer an, lassen sich zwei Erkenntnisse daraus gewinnen:
- Es können ein paar weitere Eigenschaften gesetzt werden, die im Wiki-Artikel der Doku nicht vermerkt sind. Zum Beispiel, ob die Mitglieder E-Mails versenden dürfen.
- Besitzt Entry zwei Methoden für die in der Doku angegebenen Eigenschaften getAuthors() und getContributors(), die jedoch nirgendwo im Code genutzt werden:
Ersteres ist positiv, da man diese Dinge beim händischen Erstellen per Web ebenfalls angeben kann:
Im Code werden diese Werte beispielsweise wie folgt abgerufen:
String str10 = parseElementStrVal(paramEntry.getExtension(QN_CONNECTIONS_MEMBER_EMAIL_PRIVILEGES));
Die Konstanten findet ihr in der Schnittstelle AtomConstants. Dort ist neben dem Attributsname auch der korrekte Namensraum angegeben:
public static final QName QN_CONNECTIONS_MEMBER_EMAIL_PRIVILEGES = new QName("http://www.ibm.com/xmlns/prod/sn", "memberEmailPrivileges", "snx");
Wenn man sich über die Werte unsicher ist, weil es z.B. nicht offensichtliche boolische Angaben sind wie in diesem Falle, hilft es, sich die aufgerufenen Methoden genauer anzuschauen. Oft enthalten diese Validierungscode, Mappings auf Enums oder geben anderweitig Hinweise darauf, welche Werte zulässig sind. Hier ist dies folgende Hilfsmethode, die aus den Zeichenketten eine passende Konstante erzeugt:
public static Community.MailPrivileges toMailPrivileges(String paramString) {
Community.MailPrivileges mailPrivileges = null;
if (paramString != null)
if (paramString.equalsIgnoreCase("canEmailEntireCommunity")) {
mailPrivileges = Community.MailPrivileges.MEMBERS_FULL_PRIVILEGES;
} else if (paramString.equalsIgnoreCase("canEmailOwnersOnly")) {
mailPrivileges = Community.MailPrivileges.MEMBERS_MAIL_OWNERS_ONLY;
} else if (paramString.equalsIgnoreCase("emailNotAllowed")) {
mailPrivileges = Community.MailPrivileges.MEMBERS_CANT_MAIL;
}
return mailPrivileges;
}
Hier sind also die Werte canEmailEntireCommunity, canEmailOwnersOnly und emailNotAllowed möglich. Sie entsprechenden den drei Auswahlfeldern in den Web-Einstellungen.
Generell können wir natürlich die Namensräume nutzen, um für diese das Log Level auf Maximum zu erhöhen. Die Einträge finden sich dann in der trace.log Datei.
Auf den ersten Blick bietet die communityAdmin.py eine weitere Alternative. Jython wird in Connections eingesetzt, um Java-Methoden aus Python-Skripten heraus aufrufen zu können. Man kann eine Reihe an administrativen Operationen über Python bzw. die wsadmin-Konsole durchführen. In createCommunity.py befindet sich auch eine Methode zum Anlegen von Communitys:
parms=[communityName, ownerName, memberRole, eAddrs, orgId]
sig = ["java.lang.String", "java.lang.String", "java.lang.Integer", "java.util.HashSet", "java.lang.String"]
rc = AdminControl.invoke_jmx(self.getMBeanName(), "createCommunityWithEmail", parms, sig)
print "createCommunity request processed"
In der Java-Anwendung gibt es dafür verschiedene Service-Klassen. In diesem Falle ist das etwa CommunitiesAdminService im Namensraum com.ibm.tango.internal.service.admin.mbean. Diese Methode scheint dem Code nach die Mitglieder zu berücksichtigen und sauber zur Community hinzuzufügen – in den Parametern lassen sich Eigentümer und Mitglieder angeben. Leider ist diese Methode nicht direkt per Web erreichbar.
Wie legen wir nun eine Community mit Mitgliedern an?
Leider hat all das nicht zum Erfolg geführt: Der hinzuzufügende Benutzer taucht nirgendwo auf. Auch das angeben von ungültigen Nutzern führt zu keinem Fehler. Offensichtlich handelt es sich um einen Softwarefehler, der diese Felder gar nicht auswertet. Als Übergangslösung bleibt daher nur, die Nutzer nachträglich hinzuzufügen. Dafür gibt es in comm.web.jar > com.ibm > tango.web > ui > actions weitere Klassen, die eine ganze Reihe von Funktionen abdecken. Relevant ist hier vor allem CommunityCreateAction.
Zunächst jedoch zur Erstellung an sich, die über folgenden Body möglich ist:
<?xml version="1.0" encoding="UTF-8"?>
<entry
xmlns="http://www.w3.org/2005/Atom"
xmlns:app="http://www.w3.org/2007/app"
xmlns:snx="http://www.ibm.com/xmlns/prod/sn">
<title type="text">U-Labs Community</title>
<summary type="text">ignored</summary>
<content type="html">Automatisch per API erstellte Beispiel-Community</content>
<category term="community" scheme="http://www.ibm.com/xmlns/prod/sn/type"></category>
<snx:communityType>public</snx:communityType>
</entry>
$ curl -s -D - -k -X POST -u max:cnx -H "Content-Type: application/atom+xml" --data "@create-community.xml" https://cnx7.u-labs.de/communities/service/atom/communities/my
HTTP/1.1 201 Created
...
Location: https://cnx7.u-labs.de/communities/service/atom/community/instance?communityUuid=fcedd4f4-4a05-45e5-968a-3f3f3af234a8
...
Um ein Mitglied hinzuzufügen, wird die Id der Community benötigt. Die API liefert in der Regel die Adresse zum angelegten Objekt im Location Headerfeld, sodass ihr sie dort aus dem Parameter kopieren könnt. Anschließend fügt ihr dies in den communityUuid Parameter der POST-Anfrage ein:
<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:snx="http://www.ibm.com/xmlns/prod/sn">
<contributor>
<email>user@u-labs.de</email>
<snx:role xmlns:snx="http://www.ibm.com/xmlns/prod/sn" component="http://www.ibm.com/xmlns/prod/sn/communities">member</snx:role>
</contributor>
</entry>
$ curl -s -D - -k -X POST -u max:cnx -H "Content-Type: application/atom+xml" --data "@add-members.xml" https://cnx7u-labs.de/communities/service/atom/community/members?communityUuid=fcedd4f4-4a05-45e5-968a-3f3f3af234a8
HTTP/1.1 201 Created
...
Die Community hat nun zwei Mitglieder:
Laut HCL-Dokumentation soll das Hinzufügen von Mitgliedern in der selben Anfrage möglich sein, wie das Anlegen. In meinen Tests hat dies jedoch trotz einiger Versuche nicht funktioniert. Ein Blick auf den Code bestätigt, dass dies anscheinend nicht implementiert wurde.