piatok 25. septembra 2009

How to deal with password hash in JBoss Seam

Recently I tried to clarify some points concerning authentication part of Seam security. Here can be found forum thread, where I gave some advice on this subject and which was inspiration for this blog entry: How to persist user?

So what it is all about? Seam provides a standard API for the management of a Seam application's users and roles, called Identity Management. In order to use Seam's Identity Management it must be configured with Identity Store. Out of the box Seam provides Jpa Identity Store which stores users and roles in database.

Apparently developer asking for help discovered identityManager component, which can be used to create new users, however in order to use this component user must be already authenticated and additionally must posses some permissions. (Permissions and authorization in general are not covered in this article.)

But this presents CATCH XXII situation when there is an empty database and there is no possibility to log in. In short, developer can not use identityManager.createUser(String name, String password) method because he can not log in and he can not log in because there are no user accounts in database to log in with.

Well no problem, developers can populate their brand new database with SQL scripts manually or in case of Seam, import-dev.sql file can be employed to do the job. But still there is one catch, Seam can automatically hash passwords in database and it is not trivial to reproduce this manually.

This article explains first shortly how to set up Jpa Identity Store using hashed password with salt and then how gather all information needed to create SQL script, which inserts first user into database.

Firstly Jpa Identity Store must be configured in components.xml descriptor like so:
<security:identity-manager identity-store="#{jpaIdentityStore}"/>
<security:jpa-identity-store 
user-class="com.acme.model.User"
role-class="com.acme.model.Role"/>
Where User class is Entity annotated with seam annotations like so:
(only getters are shown, fields and setters are omitted for clarity)
@Entity
public class User implements Serializable {
...
 @NotNull
 @UserPrincipal
 @Column(unique = true)
 public String getUsername() { return username; }

 @UserPassword
 public String getPasswordHash() { return passwordHash; }

 @PasswordSalt
 public String getSalt() { return salt; }

 @UserEnabled
 public boolean isEnabled() { return enabled; }

 @UserFirstName
 public String getFirstname() { return firstname; }

 @UserLastName
 public String getLastname() { return lastname; }

 @UserRoles
 @ManyToMany
 public List<UserRole> getRoles() { return roles; }
...
}
Most noticeable annotations are @UserPrincipal, @UserPassword and @PasswordSalt.
In field annotated @UserPrincipal username is stored.
In field annotated @PasswordSalt salt is stored.
In field annotated @UserPassword hashed password is stored.
Password hash depends on these parameters:

passwordHash = f(password, algorithm, salt, iterations)

Where:

password - plain password
algorithm - hashing algorithm, in Seam 2.2.0.GA, HmacSHA1 is default
salt - salt value used to produce the password hash, when not specified field annotated with @UserPrincipal is used (username is used as salt for hashing password)
iterations - number of iterations for generating the password hash, default value is 1000

Solution:

I have created small test which does the trick, it uses Seam PasswordHash component and it produces valid password, salt and hash combination which can be used in SQL script. This code was tested only with Seam 2.2.0.GA. (This version uses default hashing algorithm, algorithm parameter can be introduced, but it depends on security provider implementation so it is out of scope for this article.)
public class PasswordHashTest extends SeamTest {

    @Test
    public void testPasswordHash() throws Exception {
        new ComponentTest() {
            @Override
            protected void testComponents() throws Exception {
                Log log = Logging.getLog(PasswordHashTest.class);
                PasswordHash passwordHash = PasswordHash.instance();

                final byte[] salt = passwordHash.generateRandomSalt();
                final int iterations = 1000;
                final String password = "admin";
                final String hash = passwordHash.createPasswordKey(password.toCharArray(), salt, iterations);

                assert hash != null : "Hash not calculated!";

                log.info("Password: " + password);
                log.info("Salt: " + BinTools.bin2hex(salt));
                log.info("Iterations: " + iterations);
                log.info("Hash: " + hash);
            }
        }.run();
    }
}
After running this test, all information needed is logged as info message. This test can be run multiple times in case more user accounts are desired.

streda 2. septembra 2009

Jednoduché generovanie manifestu pomocou nástroja ANT


Pohľad na bojové pole
Na jednej strane je J2EE aplikácia zabalená ako EAR, vygenerovaná seam-gen skriptom, ktorá obsahuje EJB modul JAR a webovú časť aplikácie WAR.
Zjednodušená štruktúra aplikácie:
  • myapp.ear
    • myapp-ejb.jar
    • lib
      • richfaces-api.jar
      • ...
    • myapp-web.war
      • WEB-INF
        • lib
          • richfaces-ui.jar
          • richfaces-impl.jar
          • ...
  • myapp.ear/lib - je adresár, ktorý obsahuje knižnice implicitne viditeľné iba z myapp-ejb.jar
  • myapp.ear/myapp-web.war/WEB-INF/lib - knižnice v tomto adresári sú naopak viditeľné len pre myapp-web.war
Na druhej strane, je celkom bežná potreba, použiť niektoré knižnice na oboch miestach. Ako riešiť túto situáciu? Skopírovanie danej knižnice do oboch lib adresárov problém nerieši. Pretože sa môže napríklad stať, že trieda z danej knižnice je návratovou hodnotu metódy session beanu použitej v servlete. V tomto prípade by si web kontajner sťažoval, že trieda, ktorú mu vrátil session bean do servletu nieje tá, ktorú skutočne očakával.

Riešenie
Jedno z riešení spočíva v tom, že knižnice z adresáru myapp.ear/myapp-web.war/WEB-INF/lib okopírujeme do myapp.ear/lib a vytvoríme manifest súbor s classpath a umiestnime ho sem myapp.ear/myapp-web.war/META-INF/MANIFEST.MF.
Modifikovaná štruktúra aplikácie:
  • myapp.ear
    • myapp-ejb.jar
    • lib
      • richfaces-api.jar
      • richfaces-ui.jar
      • richfaces-impl.jar
      • ...
    • myapp-web.war
      • META-INF
        • MANIFEST.MF
Pričom obsah súboru MANIFEST.MF by vyzeral takto:

Class-Path: lib/richfaces-impl.jar lib/richfaces-ui.jar

Za povšimnutie stojí, že cesta v manifeste, je relatívna ku koreňu EAR súboru. Výsledkom je, že všetky knižnice sú viditeľné pre EJB modul a pre web modul sú viditeľné knižnice richfaces-impl.jar a richfaces-ui.jar. Tento manifest súbor sa dá samozrejme napísať aj ručne, ale zvyčajne je v classpath-e oveľa viac záznamov a keďže povolená dĺžka riadku je v manifest špecifikácii je obmedzená ľahko sa môže stať, že ručne napísaný manifest bude chybný.

Generovanie MANIFEST.MF s classpath pomocou ANT
Projekt vygenerovaný seam-gen skriptom používa na kopírovanie knižníc do war súboru následujúcu časť kódu.
<copy todir="${war.dir}/WEB-INF/lib">
  <fileset dir="${lib.dir}">
      <includesfile name="deployed-jars-war.list"/>
      <exclude name="jboss-seam-gen.jar"/>
      <exclude name="jboss-seam-debug.jar" unless="is.debug"/>
  </fileset>
</copy>
  • deployed-jars-war.list je textový súbor, ktorý obsahuje zoznam knižníc, ktoré sa kopírujú do myapp.ear/myapp-web.war/WEB-INF/lib (na jednom riadku je jedno meno knižnice)
V tomto prípade by deployed-jars-war.list vyzeral takto:

richfaces-impl.jar
richfaces-ui.jar


Modifikovaný ANT skript, ktorý nič nekopíruje, ale namiesto toho vytvorí manifest súbor s classpath záznamom.
<path id="war.classpath.path">
   <fileset dir="${lib.dir}">
       <includesfile name="deployed-jars-war.list"/>
       <exclude name="jboss-seam-gen.jar"/>
       <exclude name="jboss-seam-debug.jar" unless="is.debug"/>
   </fileset>
</path>

<manifestclasspath property="war.classpath" jarfile="${project.name}_war">
   <classpath refid="war.classpath.path" />
</manifestclasspath>

<mkdir dir="${war.dir}/META-INF"/>

<manifest file="${war.dir}/META-INF/MANIFEST.MF">
   <attribute name="Class-path" value="${war.classpath}"/>
</manifest>

nedeľa 2. augusta 2009

JBoss clustering konfigurácia pre jeden počítač

Tento tutoriál by mal pomôcť pri nastavení jednoduchého JBoss klastra na jednej pracovnej stanici s operačným systémom Windows Vista. Konfigurácia má význam iba pre programátorov, ktorí potrebujú otestovať, alebo vyvíjať svoj program na klastri, ale nemajú k dispozícii viac počítačov.

Konfigurácia Microsoft Loopback adaptéru s dvoma IP adresami
  1. Ovládací panel/Pridanie hardvéru
  2. Inštalovať hardvér vybratý manuálne v zozname
  3. Vybrať - Sieťové adaptéry
  4. Výber sieťového adaptéra/Microsoft/Microsoft Loopback Adapter
  5. Premenovanie názvu siete na "Loopback" kôli prehľadnosti - Ovládací panel/Sieťové pripojenia
  6. Sieťové pripojenie Loopback/Vlastnosti
  7. Internet Protocol Version 4 (TCP/IPv4)
  8. Nastavenie prvej IP adresy 192.168.1.140 (maska podsiete 255.255.255.0), na ktorej bude počúvať node1
  9. Spresniť/Pridať - Nastavenie druhej IP adresy 192.168.1.141 (maska podsiete 255.255.255.0), na ktorej bude počúvať node2
  10. Otvorenie Windows konzoly/príkaz ipconfig /all, ak je adaptér dobre nainštalovaný a sú nakonfigurované obe IP adresy výstup bude vypadať obdobne ako na obrázku

Inštalácia aplikačného serveru JBoss
  1. Aplikačný server je možné stiahnuť na tejto adrese: http://www.jboss.org/jbossas/downloads/
  2. Rozpakovať/Vytvoriť dva nové profily, node1 a node2, profily vytvoríme tak, že okopírujeme profil all do dvoch nových adresárov node1 a node2 (profily sa nachádzajú v adresári %JBOSS_HOME%/server), adresárová štruktúra by mala vypadať ako na obrázku
  3. V konzole príkaz %JBOSS_HOME%/bin/run.bat -c node1 -b 192.168.1.140 -Djboss.messaging.ServerPeerID=1 spustí profil node1, ktorý bude počúvať na adrese 192.168.1.140 a -Djboss.messaging.ServerPeerID=1 nastaví unikátne ID pre messaging servis, v konzole by mal byť výpis, ktorý hovorí, že klaster má jedného člena, obdobne ako na obrázku
  4. V druhom konzolovom okne spustíme príkaz %JBOSS_HOME%/bin/run.bat -c node2 -b 192.168.1.141 -Djboss.messaging.ServerPeerID=2 a v logu by sme mali vidieť hlásenie o tom, že klaster má už dvoch členov


Ak sa všetko podarilo, tak node1 v prvej konzole by mal zareagovať obdobným logom ako je tento:
18:00:38,092 INFO  [DefaultPartition] New cluster view for partition DefaultPartition (id: 1, delta: 1) : [192.168.1.140:1099, 192.168.1.141:1099]
18:00:38,092 INFO  [RPCManagerImpl] Received new cluster view: [192.168.1.140:49924|1] [192.168.1.140:49924, 192.168.1.141:51781]
18:00:38,108 INFO  [DefaultPartition] I am (192.168.1.140:1099) received membershipChanged event:
18:00:38,108 INFO  [DefaultPartition] Dead members: 0 ([])
18:00:38,108 INFO  [DefaultPartition] New Members : 1 ([192.168.1.141:1099])
18:00:38,108 INFO  [DefaultPartition] All Members : 2 ([192.168.1.140:1099, 192.168.1.141:1099])

nedeľa 26. júla 2009

How to configure PostgreSQL datasource in Embedded JBoss

I have spent more than hour fighting this issue. I will wrote it down rather than forgetting about it. So that in case I have to do this again I will know where to look, and I hope that it might be helpful for others as well.

Environment: Seam 2.1.2, PostgreSQL, JBoss embedded

Scenario: Project generated by seam-gen originally used Hypersonic SQL database (HSQLDB). Later Postgre SQL database (PGSQL) was installed and properly configured with dev build profile. However tests which are running in embedded JBoss still use HSQLDB, the goal is to configure tests to run on PGSQL as well.

Documentation says: By default, a generated project will use the java:/DefaultDS (a built in HSQL datasource in Embedded JBoss) for testing. If you want to use another datasource place the foo-ds.xml into bootstrap/deploy directory.

Sounds easy considering that dev profile is already configured with PGSQL datasource which is woking with ordinary JBoss 5.1.0. One would assume that it is sufficient to copy myproject-dev-ds.xml file to bootstrap/deploy directory (rename it to myproject-test-ds.xml for clarity sake), then change persistence-test.xml according to persistence-dev.xml and all should work.

But in case this is done, running ant test task produces only this incomprehensible error:
org.jboss.deployers.client.spi.IncompleteDeploymentException: Summary of incomplete deployments (SEE PREVIOUS ERRORS FOR DETAILS):
*** CONTEXTS MISSING DEPENDENCIES: Name -> Dependency{Required State:Actual State}
jboss.jca:name=myProjectDatasource,service=DataSourceBinding
-> jboss:service=invoker,type=jrmp{Start:** NOT FOUND **}
-> jboss:service=invoker,type=jrmp{Create:** NOT FOUND **}
*** CONTEXTS IN ERROR: Name -> Error
jboss:service=invoker,type=jrmp -> ** NOT FOUND **
at org.jboss.deployers.plugins.deployers.DeployersImpl.checkComplete(DeployersImpl.java:576)
at org.jboss.deployers.plugins.main.MainDeployerImpl.checkComplete(MainDeployerImpl.java:559)
at org.jboss.embedded.Bootstrap.bootstrapURL(Bootstrap.java:149)
at org.jboss.embedded.Bootstrap.bootstrap(Bootstrap.java:183)
at org.jboss.embedded.Bootstrap.bootstrap(Bootstrap.java:195)
at org.jboss.seam.mock.EmbeddedBootstrap.startAndDeployResources(EmbeddedBootstrap.java:11)
at org.jboss.seam.mock.AbstractSeamTest.startJbossEmbeddedIfNecessary(AbstractSeamTest.java:1024)
at org.jboss.seam.mock.AbstractSeamTest.startSeam(AbstractSeamTest.java:915)
at org.jboss.seam.mock.SeamTest.startSeam(SeamTest.java:58)
... Removed 15 stack frames
where myproject-test-ds.xml looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<datasources>
<local-tx-datasource>
  <jndi-name>myProjectDatasource</jndi-name>

  <use-java-context>false</use-java-context><!-- note this line -->

  <connection-url>jdbc:postgresql://localhost:5432/test</connection-url>
  <driver-class>org.postgresql.Driver</driver-class>
  <user-name>test</user-name>
  <password>test</password>
</local-tx-datasource>
</datasources>
and persistence-test.xml looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Persistence deployment descriptor for test profile -->
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
       version="1.0">
<persistence-unit name="myProject">
  <provider>org.hibernate.ejb.HibernatePersistence</provider>

  <jta-data-source>myProjectDatasource</jta-data-source><!-- note this line -->

  <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="jboss.entity.manager.factory.jndi.name" value="java:/myProjectEntityManagerFactory"/>
  </properties>
</persistence-unit>
</persistence>
So both files seem to be configured properly, how come it is not working then? Solution can be found here: https://jira.jboss.org/jira/browse/JBPAPP-2223.

Workaround Description: remove "false" from the datasource file and add "java:/" prefix to the content of jta-data-source element in persistence.xml

And indeed after changing myproject-test-ds.xml like this:
<?xml version="1.0" encoding="UTF-8"?>
<datasources>
 <local-tx-datasource>
     <jndi-name>myProjectDatasource</jndi-name>
     <connection-url>jdbc:postgresql://localhost:5432/test</connection-url>
     <driver-class>org.postgresql.Driver</driver-class>
     <user-name>test</user-name>
     <password>test</password>
 </local-tx-datasource>
</datasources>
and persistence-test.xml like this:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Persistence deployment descriptor for test profile -->
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
          version="1.0">
 <persistence-unit name="myProject">
     <provider>org.hibernate.ejb.HibernatePersistence</provider>

     <jta-data-source>java:/myProjectDatasource</jta-data-source><!-- note this line -->

     <properties>
         <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
         <property name="hibernate.show_sql" value="true"/>
         <property name="hibernate.format_sql" value="true"/>
         <property name="jboss.entity.manager.factory.jndi.name" value="java:/myProjectEntityManagerFactory"/>
     </properties>
 </persistence-unit>
</persistence>
everything works like a charm.

piatok 24. júla 2009

Ako na to HTTPS, JBoss a Seam

Toto je krátky návod, ktorý by mal pomôcť pri konfigurácii HTTPS komunikácie vo vývojom prostredí, ktoré obsahuje aplikačný server JBoss 5.1.0.GA a aplikáciu vygenerovanú nástrojom seam-gen vo verzii 2.1.2.
  1. Vygenerovanie "selfsigned" certifikátu ak nemáme žiadny k dispozícii.
  2. Nastavenie JBoss HTTPS konektora.
  3. Konfigurácia stránok v Seame.
Vygenerovanie "selfsigned" certifikátu ak nemáme žiadny k dispozícii.

Na generovanie použijeme nástroj keytool, ktorý sa štandardne nachádza v Sun JDK.
cd %JAVA_HOME%\bin

C:\Program Files\Java\jdk1.6.0_10\bin\keytool -genkeypair -alias devServerCert -keyalg RSA -validity 1500 -keystore devServer.keystore

Enter keystore password: devserver
Re-enter new password: devserver
What is your first and last name?
[Unknown]:  Meno Priezvisko
What is the name of your organizational unit?
[Unknown]:  oddelenie
What is the name of your organization?
[Unknown]:  spolocnost
What is the name of your City or Locality?
[Unknown]:  mesto
What is the name of your State or Province?
[Unknown]:  stat
What is the two-letter country code for this unit?
[Unknown]:  sk
Is CN=Meno Priezvisko, OU=oddelenie, O=spolocnost, L=mesto, ST=stat, C=sk correct?
[no]:  yes

Enter key password for devservercert enter
 (RETURN if same as keystore password):
Je potrebné si uvedomiť, že heslo pre kľúč je také isté ako heslo pre keystore, preto je ako vstup na konci iba enter. Vygenerovaný súbor devServer.keystore okopírujeme do ...jboss-5.1.0.GA\server\default\conf\ za predpokladu, že používame profil default.

Nastavenie JBoss HTTPS konektora.

Za predpokladu, že používame profil default je potrebné zmeniť súbor ...\jboss-5.1.0.GA\server\default\deploy\jbossweb.sar\server.xml takto:
...
   <Connector protocol="HTTP/1.1" SSLEnabled="true"
        port="8443" address="${jboss.bind.address}"
        scheme="https" secure="true" clientAuth="false"
        keystoreFile="${jboss.server.home.dir}/conf/devServer.keystore"
        keystorePass="devserver" sslProtocol = "TLS" />
...
kde:
keystoreFile - je cesta kde je uloženýdevServer.keystore súbor
keystorePass - je heslo pre keystore a zároveň aj pre klúč v ňom uložený

Konfigurácia stránok v Seame.
  • konfigurácia portov pre HTTP (8080) a HTTPS (8443) v štandartnom konfiguračnom súbore pages.xml
<?xml version="1.0" encoding="UTF-8"?>
<pages xmlns="http://jboss.com/products/seam/pages"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.1.xsd"

    no-conversation-view-id="/home.xhtml"
    login-view-id="/login.xhtml"
    http-port="8080"
    https-port="8443">
...
  • nastavenie pre konkrétnu stránku aby sa zobrazovala cez HTTPS protokol (v prípade, že request bude požadovať HTTP presmeruje sa na HTTPS), napríklad pre stránku login.xhtml stačí pridať scheme="https" do súboru login.page.xml
<?xml version="1.0" encoding="UTF-8"?>
<page xmlns="http://jboss.com/products/seam/pages"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.1.xsd"
   scheme="https">
...

utorok 14. júla 2009

Nová firemná web stránka

Od dnes je k dispozícii nová firemná web stránka na www.zont.eu.

Announcing new company web page

Since today there is a new company web page available at www.zont.eu.

utorok 7. júla 2009

New blog structure

After observing other companies successfully integrate a blog into their enterprise web pages, I made decision that this blog will become inseparable part of my company's web page which is being created at the moment.

The main goal is to bring a life into the not really very interesting static web presentation of the company, which in my opinion was already done successfully by others.

I plan to publish in this blog mainly technical solutions to problems which I have to tackle with, but which might be of interest for others as well, to a lesser degree I plan to publish news concerning the company as such.

Nové zameranie blogu

Po vzore iných spoločností, ktoré integrovali blog do svojej firemnej web stránky som sa rozhodol, že tento blog sa stane taktiež nedeliteľnou súčasťou mojej firemnej web stránky, ktorú v súčastnosti pripravujem.

Hlavným cieľom je oživiť nie príliš zaujímavú statickú webovú prezentáciu spoločnosti, tak ako sa to podľa môjho názoru podarilo iným.

Do blogu plánujem prispievať hlavne technickými riešeniami problémov, s ktorými sa stretnem, ale ktoré by mohli byť užitočné aj pre niekoho iného, taktiež v menšej miere novinkami ohľadom spoločnosti.

streda 1. júla 2009

Workaround for GWT Menubar issue 374

Issue link: Command based top level MenuItems (no sub-menu) stay highlighted when mouse leaves MenuBar.

Short description:

MenuItems on main MenuBar that have no sub-menus stay highlighted even after the mouse leaves the MenuBar. For example the fragment below produces just such a menu...


Workaround:

All workarounds provided on issue page required users to maintain their own GWT builds, which is pretty annoying for such a small nuisance. I have slightly modified one of those solutions, so that now you can package it into your application. It breaks some good practices concerning encapsulation, but it seems to work quite well with GWT 1.6.4. However use only at your own risk.
package com.yourcompany.gwtfix;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;

public class FixedMenuBar extends MenuBar {
    private static final String DEPENDENT_STYLENAME_SELECTED_ITEM = "selected";

    public void onBrowserEvent(Event event) {
        super.onBrowserEvent(event);
        MenuItem item = myFindItem(DOM.eventGetTarget(event));
        if (item == null) {
            return;
        }

        if (DOM.eventGetType(event) == Event.ONMOUSEOUT) {
            this.setSelectionStyle(item, false);
        } else if (DOM.eventGetType(event) == Event.ONMOUSEOVER) {
            this.setSelectionStyle(item, true);
        }
    }

    private void setSelectionStyle(MenuItem item, boolean selected) {
        if (selected) {
            item.addStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM);
        } else {
            item.removeStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM);
        }
    }

    private MenuItem myFindItem(Element hItem) {
        for (MenuItem item : getItems()) {
            if (DOM.isOrHasChild(item.getElement(), hItem))
                return item;
        }
        return null;
    }

    public FixedMenuBar() {
        super();
    }

    public FixedMenuBar(boolean autoClose) {
        super(autoClose);
    }
}

nedeľa 7. júna 2009

Jemný úvod do Jess

Zhodou okolností sa mi podarilo pri modifikovaní svojho súborového systému zmazať tú nesprávnu partíciu na disku, niečo také vždy poteší. :)
Ale všetko zlé je na niečo dobré a pri prehľadávaní mojich archívov som medzi starými CD-čkami našiel svoju starú (2005) diplomovú prácu a rozhodol som sa, že predtým než ju zase niekde stratím, radšej ju zverejním.

Niektoré časti mi už v dnešnej dobe pripadajú úsmevné, ale v prípade, že poznáte napríklad JBoss Rules (Drools) a chceli by ste vedieť čo to vlastne je pravidlový systém (rule engine) a ako principiálne pracuje Rete algoritmus, tak si pozrite prvú kapitolu.

V práci nieje zmienka o Drools, pretože som tam používal Jess (Java Expert System Shell), ale oba systémy sú založené na vylepšených implementáciách Rete algoritmu.

Myslím, že je to písané celkom jednoduchou a pochopiteľnou formou, ale Jess bol pôvodne klonom CLIPSu a jeho syntax sa podobá na LISP, takže typicky bloky kódu sú vymedzené okrúhlymi zátvorkami a taktiež je tam prefixový zápis operátorov.

Link na stiahnutie: Diplomová práca (2005)

streda 3. júna 2009

Ako na geolokáciu s WorldIP API

Pojem geolokácia (Geolocation) v tomto kontexte znamená určenie reálnej geografickej polohy počítača podľa jeho IP adresy.

Požiadavka:

Web stránka podporuje viac jazykových mutácií, medzi ktorými si užívateľ môže vybrať jazyk, ktorému rozumie. Pri otvorení stránky sa implicitne vyberie jazyk, ktorým sa hovorí v krajine, z ktorej je užívateľ pripojený na internet. Konkrétne, ak máme web stránku, ktorá podporuje anglický a slovenský jazyk, výsledok bude, že užívateľovi zo Slovenska, alebo Čiech sa načíta slovenská verzia a pri vstupe na stránku z iných krajín sa zobrazí anglická verzia.

Riešenie:

Riešenie by malo byť jednoduché, zadarmo, rýchlo realizovateľné a dostatočne univerzálne.
Malo by bežať na Google App Engine (GAE) s klientom v Google Web Toolkit (GWT).

Na prvý pohľad sa mi zapáčilo WorldIP API, pretože formát HTTP dotazu je veľmi jednoduchý a výsledkom je iba dvojmiestny kód krajiny. Teda nieje potrebné parsovať žiadne XML a neprenášajú sa iné zbytočné údaje.

Formát dotazu je:

http://api.wipmania.com/[IPADDR]?[URL]

kde:

IPADDR - je IP adresa, ktorej geografickú lokáciu chceme zistit URL - je doména našej aplikácie, slúži na kontrolu denných limitov

príklad: Aplikácia myappid.appspot.com sa dotazuje na pôvod IP adresy 123.45.67.89

http://api.wipmania.com/123.45.67.89?myappid.appspot.com

výsledok:

dvojznakový kód krajiny, napr. "SK" pre Slovensko.

Jednoduchá pomocná trieda, ktorá funguje aj na GAE:
public class WorldIPHelper {
...
 private static final String WIP_API_URL = "http://api.wipmania.com/";
 private static final String DOMAIN_NAME_PARAMETER = "?myappid.appspot.com";
...
 public static String getLocaleFromWorldIP(String remoteIPAddress) {
  String countryCode = "";
  try {
   URL url = new URL(WIP_API_URL + remoteIPAddress + DOMAIN_NAME_PARAMETER);
   BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
   char[] cbuf = new char[2];
   reader.read(cbuf);
   countryCode = new String(cbuf);
   reader.close();
  } catch (MalformedURLException e) {
   log.severe(e.toString());
  } catch (IOException e) {
   log.severe(e.toString());
  }
  return countryCodeToLocale(countryCode).getLocaleString();
 }
}

JSP stránka, ktorá nastavuje lokalizačnú vlastnosť GWT podľa IP adresy klienta.
...
<%
...
 localeString = WorldIPHelper.getLocaleFromWorldIP(request.getRemoteAddr());
...
%>
...
<meta name="gwt:property" content="locale=<%=localeString%>">
...

Limity:

- pre GAE platí hlavne skupina kvót UrlFetch - pre WorldIP API je to 10 000 dotazov denne na jednu aplikáciu

Ak by dané limity WorldIP API neboli dostačujúce, prípadne by nebolo vhodné sa z nejakého dôvodu spoliehať na externú službu, je tu tiež možnosť stiahnuť si priamo databázu IP adries, naimportovať si ju do vlastnej databázy a naimplementovať si vyhľadávanie vo vlastnej réžii.

Ak ste niekedy riešili niečo podobné, tak odporučte akú službu ste použili, prípadne či ste s ňou boli spokojní.

štvrtok 30. apríla 2009

CR4CK ME (časť 1.)

Tento blog voľne nadväzuje na predchádzajúcu programátorskú úlohu, tentokrát sa pozriem na úlohu pre analytikov počítačových infiltrácií. Ako Java programátor som našťastie v bežnom živote vzdialený milióny riadkov kódu od assembleru, ale kto by odolal výzve. :)

! VAROVANIE: tento článok obsahuje riešenie, alebo jeho časti, ktoré môžu, ale nemusia byť správne, každopádne ak ste úlohu ešte neriešili a chcete si ju urobiť samostatne, tak ďalej nečítajte a vráťte sa k tomuto textu až keď si budete chcieť porovnať svoje zistenia !

Zadanie úlohy je možné nájsť na http://www.3537.sk/virusovy-analytik.html a skrátene znie takto:

Predložená úloha - program s názvom ESET_crackme.exe bol navrhnutý za účelom otestovania vašich schopností v oblasti reverzného inžinierstva programov. Vašou úlohou je vykonať analýzu tohto programu (jeho programového kódu). Analýza programu (programového kódu) podáva komplexné informácie o činnosti programu, podmienkach potrebných pre vykonanie určitých akcií v programe a podobne. Programový kód môže obsahovať skryté texty, podmienené úlohy, ochranu pred ladiacimi programami a pod.

Prvá vec, ktorú vyskúšam je proste EXE súbor iba spustiť, aby som vedel ako sa správa keď ho neladím a aby som vedel ako sa javí bežnému užívateľovi.
  • prvý pokus...rezidentná ochrana môjho antivíru mi zablokovala prístup k súboru s tým že je to možno vírus, :) mno nič vypnem antivír
  • druhý pokus...rezidentná ochrana môjho anti-adware programu mi zablokovala prístup k súboru, taktiež som ju vypol
  • tretí pokus...program vypíše chybové hlásenie, v zmysle, že mu chýba knižnica rtl70.bpl, nevadí stiahnem požadovanú knižnicu a nakopírujem ju do systémového adresára
  • štvrtý pokus...hurá :) program vypíše informačný text do konzoly, počká si na stlačenie klávesy a skončí
Pamätám si, že kedysi strašne dávno som si naprogramoval niečo v C++ Builderi 3 a keď som sa s tým chcel pochváliť kamarátovi, tak ma čakalo podobné nemilé prekvapenie, u mňa samozrejme program bežal, ale u každého kto nemal nainštalovaný C++ Builder alebo Delphi bol problém. Každopádne môj prvý dojem z programu je taký, že bol asi napísaný ako konzolová aplikácia v Pascale konkrétne v Delphi 7 a ak v ňom bol použitý assembler, tak asi len inline priamo v Pascalovských zdrojákoch.

Predtým ako má vôbec zmysel začať ladiť tento program je potrebné zistiť či je skomprimovaný a keď áno tak čím. Na tento účel existuje určite množstvo nástrojov, ja som si zvolil PEiD.



Ako je zrejmé z obrázkov, tak po miernej zmene nastavenia z Normal Scan na Hardcore Scan, PEiD identifikuje, že súbor je skomprimovaný programom UPX. Čo je dobrá správa, pretože UPX je open source, to znamená, že je ho možné voľne stiahnuť, existuje k nemu verejná dokumentácia a sú verejné aj jeho zdrojové kódy. Najjednoduchší spôsob ako teda súbor rozpakovať je stiahnuť si UPX a skúsiť to automaticky.



Hmmm, až také ľahké to nebude, zrejme bola PE hlavička nejakým spôsobom modifikovaná, ale "vygooglit" si podrobný návod ako rozpakovať UPX manuálne dokonca aj s odkazmi na zdrojový kód UPX a vysvetlivkami je len otázkou chvíľky. V návode pre riešiteľov sa odporúča ako veľmi dobrý ladiaci program OllyDbg, tak v ňom teda otvorím ESET_crackme.exe a privíta ma hlásením, ktoré ma upozorňuje, že analýza kódu bude zrejme chybná, lebo telo programu je skomprimované.



Návod na manuálne rozbalenie UPX archívu hovorí, že je potrebné hľadať inštrukciu POPAD (inštrukcia obnoví registre pre všeobecné použitie zo zásobníka, je to prakticky opak inštrukcie PUSHAD, na ktorej sa práve nachádzame), ktorú nasleduje inštrukcia JMP (inštrukcia urobí nepodmienený skok). Tak si vyhľadáme [Ctrl-S], [] Entire block, POPAD.



Na ďalšom obrázku je vidieť, že sme úspešne našli inštrukciu POPAD, za ktorou sa nachádza nepodmienený skok JMP a práve naň si dáme breakpoint [F2].



Spustíme program [F9] až po breakpoint a posunieme sa ešte o jednu inštrukciu [F7] cez náš JMP a mali by sme mať rozpakovaný program a zároveň by sme mali byť nastavení na originálnom vstupnom bode aplikácie.



Keďže by bolo zbytočné tento postup opakovať zakaždým keď budeme ladiť program, tak použijeme plugin na dumpovanie procesov a vytvoríme si týmto spôsobom nový EXE súbor, ale už rozpakovaný a s novým entry pointom ako je to na nasledujúcich dvoch obrázkoch.



Keď už máme rozpakovaný program, urobíme si na ňom takú predbežnú analýzu, aby sme zistili aký má potenciál. Napríklad, pracuje zo súbormi? Zapisuje alebo číta niečo z registrov windows? Komunikuje s inými počítačmi? Na to aby sme zistili či má program potenciál robiť nasledovné veci si ho otvoríme v programe IDA Pro Free. A budeme skúmať aké knižnice používa, aké funkcie windows volá a aké textové reťazce obsahuje, ale to až v budúcej časti, ak si nájdem čas a ak sa mi bude chcieť. :)

sobota 11. apríla 2009

PR0GR4M470R5K4 UL0H4

V poslednom čase sa v reálnom živote, ale aj na internete rozpútala zúrivá reklamná kampaň, ktorá má okrem iného osloviť aj programátorov. Nemohol som si ju nevšimnúť, pretože každý deň chodím okolo veľkého reklamného nápisu:

BUĎTE LEPŠÍ AKO VŠETCI HACKERI SVETA.

Všimol som si, že táto reklama obsahuje aj malú programátorskú úlohu. A keďže každý programátor bez výnimky si myslí, že je najlepší na svete, tak ani ja som nemohol odolať a musel som ju kvôli spokojnému spánku vyriešiť.

Znenie úlohy je možné nájsť tu http://www.3537.sk/programator.html, citujem:

Máte zoznam súborov, v ktorom je každý súbor identifikovaný menom a podpisom. Meno je ľubovoľný reťazec v úvodzovkách a podpis je postupnosť nezáporných 32-bitových čísiel v šestnástkovej sústave ako môžete vidieť na obr. 01.

"test_name" 3a 45ffa2 236da0 34cc 21
"/usr/stdio.h" 456fa 34dd 28ab9 457e
".other.txt" 5623d ff3a21 783d 22456

Obr. 01: formát zoznamu súborov

Vašou úlohou je nájsť počet jedinečných súborov v zozname. Súbor je považovaný za jedinečný, ak jeho podpis nie je riadnym prefixom podpisu akéhokoľvek iného súboru v zozname. Podpis s1 je považovaný za riadny prefix podpisu s2 vtedy (a len vtedy), keď s2 začína s s1 a podpisy majú rôzne dĺžky. V prípade, že viaceré súbory majú rovnaký podpis, započítajte ich všetky.
Úlohu som rozdelil na dve časti:
  1. načítanie celého súboru do pamäte a rozparsovanie obsahu do vhodnej dátovej štruktúry
  2. algoritmus, ktorý počíta požadované hodnoty nad touto dátovou štruktúrou
Ďalej sa bodom 1 nebudem zaoberať, pretože ho nepovažujem za dôležitý. Predpokladajme, že máme už celý obsah súboru uložený v List<FileDescription>, kde FileDescription vyzerá takto:
public class FileDescription {
    private String name;
    private List<Long> signature = new ArrayList<Long>();

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getSignatureSize() { return signature.size(); }
    public Long getSignature(int index) { return signature.get(index); }
    public void addSignature(long signature) { this.signature.add(signature); }
}
Je to obyčajný value object, ktorý obsahuje meno súboru, a podpis, ktorý je List Long-ov, pretože do Java int-u sa 32-bitové číslo nezmestí. (kvôli znamienku)

Riešenie 1 (Java):

Najjednoduchšie riešenie spočíva v tom, že najprv zoradíme FileDescription prvky v Liste podľa veľkosti podpisu od najväčšieho po najmenší a potom prejdeme každý prvok v Liste od začiatku a skontrolujeme, či má nejaké duplikáty. Ak na nejaký duplikát narazíme, proste ho z Listu vymažeme. Na konci metódy nám v Liste ostanú iba unikátne (jedinečné) súbory.
    private static class FileDescriptionComparatorLargestFirst implements Comparator<FileDescription>, Serializable {
        public int compare(FileDescription fileDescription1, FileDescription fileDescription2) {
            if (fileDescription1.getSignatureSize() < fileDescription2.getSignatureSize()) return 1;
            if (fileDescription1.getSignatureSize() > fileDescription2.getSignatureSize()) return -1;
            return 0;
        }
    }

    public int calculateNumberOfUnique1P(List<FileDescription> fileDescriptions) {
        Collections.sort(fileDescriptions, new FileDescriptionComparatorLargestFirst());
        for (int i = 0; i < fileDescriptions.size(); i++) {
            FileDescription outerFileDescription = fileDescriptions.get(i);
            for (int j = i + 1; j < fileDescriptions.size(); j++) {
                FileDescription innerFileDescription = fileDescriptions.get(j);
                if (outerFileDescription.getSignatureSize() > innerFileDescription.getSignatureSize()) {
                    boolean equal = true;
                    for (int k = 0; k < innerFileDescription.getSignatureSize(); k++) {
                        if (!innerFileDescription.getSignature(k).equals(outerFileDescription.getSignature(k))) {
                            equal = false;
                            break;
                        }
                    }
                    if (equal) {
                        fileDescriptions.remove(innerFileDescription);
                        j--;
                    }
                }
            }
        }
        return fileDescriptions.size();
    }
  • algoritmus využíva 3 vnorené for cykly, čo na prvý pohľad naznačuje, že nebude príliš výkonný, ale malý test súbor set_small.dat spracuje bez problémov v zlomku sekundy
  • taktiež výsledok zahŕňa List s unikátnymi súbormi a nielen ich počet (aj keď metóda vracia iba číslo)
  • vyskúšal som metódu aj na veľkom súbore set_large.dat, ale aby sa súbor vôbec načítal do pamäte je potrebné pridať pamäť pre JVM oproti štandardnému nastaveniu, napríklad takto -Xms128m -Xmx1024m
  • kým sa algoritmus dopracoval k výsledku prešlo viac než 20 minút, takže "Tudy cesta nevede." :)

Riešenie 2 (Java):

Druhý algoritmus rozdelí súbory z Listu do Mapy Listov podľa ich podpisu, najprv podľa prvého čísla v podpise a potom odstráni na tej úrovni všetky duplikáty a unikátne súbory z Listu a opakuje tú istú procedúru pre všetky Listy v Mape rekurzívne. Týmto spôsobom sa dá dosiahnuť minimalizácia porovnávania podpisov jednotlivých súborov.
    public int calculateNumberOfUnique2P(List<FileDescription> fileDescriptions) {
        List<FileDescription> uniques = new ArrayList<FileDescription>();
        List<FileDescription> duplicates = new ArrayList<FileDescription>();

        recurseFileDescriptionListP(fileDescriptions, 0, uniques, duplicates);

        return uniques.size();
    }

    private void recurseFileDescriptionListP(List<FileDescription> fileDescriptions, Integer level, List<FileDescription> uniques, List<FileDescription> duplicates) {
        if (level > 0) {
            if (fileDescriptions.size() == 1) {
                uniques.add(fileDescriptions.get(0));
                return;
            }
            if (fileDescriptions.size() > 0) {
                List<FileDescription> cleanCandidates = new ArrayList<FileDescription>();
                Integer largestSizeInList = 0;
                for (FileDescription fileDescription : fileDescriptions) {
                    if (fileDescription.getSignatureSize() == level) {
                        cleanCandidates.add(fileDescription);
                    }
                    if (fileDescription.getSignatureSize() > largestSizeInList) {
                        largestSizeInList = fileDescription.getSignatureSize();
                    }
                }
                if (cleanCandidates.size() > 0) {
                    if (largestSizeInList.equals(level)) {
                        uniques.addAll(cleanCandidates);
                        return;
                    }
                     if (largestSizeInList > level) {
                        duplicates.addAll(cleanCandidates);
                        fileDescriptions.removeAll(cleanCandidates);
                    }
                }
            }
        }

        Map<Long, List<FileDescription>> longFileDescriptionMap = new HashMap<Long, List<FileDescription>>();

        for (FileDescription fileDescription : fileDescriptions) {
            Long key = fileDescription.getSignature(level);
            List<FileDescription> fileDescriptionList = longFileDescriptionMap.get(key);
            if (fileDescriptionList == null) {
                fileDescriptionList = new ArrayList<FileDescription>();
                longFileDescriptionMap.put(key, fileDescriptionList);
            }
            fileDescriptionList.add(fileDescription);
        }

        for (List<FileDescription> fileDescriptionList : longFileDescriptionMap.values()) {
            recurseFileDescriptionList(fileDescriptionList, level + 1, uniques, duplicates);
        }
    }
  • aby sme zabránili pretečeniu zásobníka pri tejto metóde je potrebné upraviť parametre JVM, napríklad takto -Xms128m -Xmx1024m -Xss64m
  • výsledkom algoritmu sú 2 nové Listy, jeden obsahuje unikátne súbory a druhý duplicitné
  • beh programu pre veľký testovací súbor set_large.dat trvá menej ako jednu sekundu
  • pomerne ľahko by sa dal prepísať aby bežal vo viacerých threadoch

Riešenie 3 (C++):

Toto riešenie je prepisom riešenia 2 do iteratívnej podoby, teda ten istý algoritmus, ale bez rekurzie a pretože je napísaný v C++, tak opakujem aj triedu FileDescription.
class FileDescription {
private:
    string name;
    vector<unsigned int> signature;
public:
    string getName() const { return name; }
    void setName(string name) { this->name = name; }
    unsigned int getSignature(unsigned int level) const { return signature[level]; }
    void addSignature(unsigned int signature) { this->signature.push_back(signature); }
    unsigned int getSignatureSize() const { return this->signature.size(); }
};
unsigned int cleanFileDescriptions3P(list<FileDescription*>& fileDescriptions, unsigned int level, unsigned int& uniques, unsigned int& duplicates) {
    if (fileDescriptions.size() == 1) {
        uniques++;
        return 0;
    }
    if (fileDescriptions.size() > 1) {
        list<FileDescription*> toRemove;
        unsigned int largestSizeInList = 0;

        for (list<FileDescription*>::iterator iter = fileDescriptions.begin(); iter != fileDescriptions.end(); ++iter) {
            unsigned int signatureSize = (*iter)->getSignatureSize();
            if (signatureSize == level+1) {
                toRemove.push_back((*iter));
            }
            if (signatureSize > largestSizeInList) {
                largestSizeInList = signatureSize;
            }
        }
        if (toRemove.size() > 0) {
            if (largestSizeInList == level+1) {
                uniques += toRemove.size();
                return 0;
            }
            if (largestSizeInList > level+1) {
                duplicates += toRemove.size();

                for (list<FileDescription*>::const_iterator iter = toRemove.begin(); iter != toRemove.end(); ++iter) {
                    fileDescriptions.remove(*iter);
                }
            }
        }
    }
    return fileDescriptions.size();
}

void calculateNumberOfUnique3P(list<FileDescription*>& fileDescriptions, unsigned int& uniques, unsigned int& duplicates) {
    unsigned int level = 0;
    map<unsigned int, list<FileDescription*> > fileDescriptionMap;
    list<list<FileDescription*> > fileDescriptionsListOfLists;

    fileDescriptionsListOfLists.push_back(fileDescriptions);

    while (!fileDescriptionsListOfLists.empty()) {
        for (list<list<FileDescription*> >::const_iterator outerListIter = fileDescriptionsListOfLists.begin(); outerListIter != fileDescriptionsListOfLists.end(); ++outerListIter) {

            for (list<FileDescription*>::const_iterator innerListIter = (*outerListIter).begin(); innerListIter != (*outerListIter).end(); ++innerListIter) {
                unsigned int key = (*innerListIter)->getSignature(level);
                fileDescriptionMap[key].push_back((*innerListIter));
            }
        }
        fileDescriptionsListOfLists.clear();

        for (map<unsigned int, list<FileDescription*> >::const_iterator mapIter = fileDescriptionMap.begin(); mapIter != fileDescriptionMap.end(); ++mapIter) {
            list<FileDescription*> fileDescriptionsPart = mapIter->second;

            unsigned int unresolvedListFlag = cleanFileDescriptions3P(fileDescriptionsPart, level, uniques, duplicates);
            if (unresolvedListFlag>0) {
                fileDescriptionsListOfLists.push_back(fileDescriptionsPart);
            }
        }
        fileDescriptionMap.clear();
        level++;
    }
}
  • výsledkom sú dve čísla a to počet duplicitných a počet unikátnych súborov
  • výkon je porovnateľný s riešením číslo 2
Záver:

Moje výsledky:
  • set_small.dat: spolu súborov 942, z toho unikátnych 851
  • set_large.dat: spolu súborov 106020, z toho unikátnych 96312
Zadávatelia úlohy výsledky nezverejnili, takže nemám možnosť si skontrolovať či som zadanie pochopil správne. Každopádne pokiaľ problém niekto riešil, určite napíšte komentár ako Vám vyšiel, prípadne aj linku na Vaše riešenie.

Čo dodať ku koncu? Netrúfam si síce povedať, že som lepší ako najlepší hackeri sveta. Ale za predpokladu, že najlepší hackeri sveta sú z Číny, som lepší ako najlepší hackeri sveta v jedení lyžičkou. :)

A čo Vy? Ste lepší ako najlepší hackeri sveta?