Cortex Tickets – Online-Ticketvorverkauf für jedermann

Als die Cortex Media GmbH startete, war ein großes Ziel von uns, neben dem Anbieten von individuellen Lösungen im Bereich Webentwicklung, Server und Internetapplikationen auch der Vertrieb von eigenen Produkten. Wir begannen deshalb relativ zügig mit einigen Brainstormings, was denn dieses erste eigene Produkte sein könnte. Auf Grund von persönlichen Erfahrungen beim Veranstalten von kleinen und größeren Theater-, Musical- und Konzertveranstaltung war die Idee schnell geboren:

Wir wollten einen kostengünstigen Online-Ticketverkauf als Alternative für kleine und mittelgroße Veranstalter schaffen, denen die Konditionen und Preise großer Anbieter nicht zusagen und die von der oftmals unnötigen Komplexität abgeschreckt sind. Auch ein Name war schnell geboren und so starteten wir im Februar 2010 mit der Entwicklung unseres eigenen Online-Ticketverkauf-Portals cortex-tickets.de.

Die Startseite unseres Portals www.cortex-tickets.de

Die Startseite unseres Portals www.cortex-tickets.de

Oberste Maxime bei der Entwicklung waren natürlich die Grundsätze, die wir auch bei all unseren Kundenprojekten anlegen: Hohe Sicherheit, ausgezeichnete Performance, Hochverfügbarkeit, Mobilität, Innovation und die Einbeziehung neuster Technologien. Außerdem war uns ein besonderes Anliegen, eine faire Preisstruktur und eine sehr einfache und intuitive Bedienoberfläche zu schaffen.

Neben der Webentwicklung des Portals bot das Projekt noch weitere spannende Herausforderungen, die viele der Themenbereiche abdecken, in denen die Cortex Media GmbH auch als Dienstleister Auftritt:

  • Der Benutzer druckt die gekauften Tickets in unserem System selbst bei sich zu Hause aus. Dafür wurde ein Modul entwickelt, das eine beliebige Anzahl von Tickets mit Bar- und QR-Codes als PDF generiert und diese dem Benutzer per E-Mail schickt bzw. zum Download bereitstellt.
  • Die Webplattform speichert Kundendaten und Bezahlinformationen. Diese Daten sind natürlich sensibler als die meisten Daten, die gewöhnliche Webseiten speichern. Wir haben deshalb hier besonderen Augenmerk auf Datenschutz und Datensicherheit gelegt.
  • Natürlich müssen auch zahlreiche Bezahlsysteme sinnvoll und sicher in die Webseite integriert werden. Cortex Tickets unterstützt inzwischen die reibungslose Zahlung mit Visa & Mastercard, Giropay und per Vorkasse.
  • Um den Arbeitsaufwand für unsere Mitarbeiter möglichst gering zu halten, mussten auch viele automatisierte Prozesse entwickelt werden, zum Beispiel die Auszahlung an den Veranstalter oder die Abgleichung der Vorkassenzahlungen. Cortex Tickets ist in der Lage viele dieser Prozesse völlig ohne menschliches Eingreifen automatisiert durchführen.
Die Verkaufsseite eines Beispielevents

Die Verkaufsseite eines Beispielevents

Ein besonders Augenmerk wollten wir auch auf möglichst einfache Einlasskontrollen legen, denn was nützt einem Veranstalter der beste Onlinevorverkauf, wenn später am Einlass Chaos entsteht. Wir haben dieses Problem durch eine Kombination aus klassischer Einfachheit und moderner Innovation gelöst:

Ganz klassisch kann der Veranstalter sich bei uns eine Gästeliste ausdrucken und die einzelnen Besucher abhaken. Er hat jedoch auch die Möglichkeit über den auf dem Ticket integrierten Barcode eine Einlasskontrolle mit einem Barcode-Scanner und unserer modernen Multiplattform-Software durchzuführen.

Als besondere Innovation bietet Cortex Tickets auch die Einlasskontrolle per iPhone und Android an. Hier benötigt der Veranstalter nur die kostenlose Cortex Tickets App und kann mit der integrierten Kamera des eigenen Smartphones die Kontrolle vornehmen.

Einlasskontrolle per Barcodescanner und Cortex Tickets Software

Einlasskontrolle per Barcodescanner und Cortex Tickets Software

Einlasskontrolle per iPad

Einlasskontrolle per iPad

Heute ist Cortex Tickets mit einem Jahresumsatz von über 100.000 € und mehreren hundert Events pro Woche eine etablierte und beliebte Ticketplattform, die sich mit den fairen und günstigen Preisen hinter keinem Konkurrenten verstecken muss.

In Unsere Projekte veröffentlicht

Android Event Bus for fun and profit

Was ist ein Event Bus und warum ist er so praktisch?

Eine typische Android-App besteht in der Regel aus einer nicht kleinen Menge an Activities, Fragments und Services. Diese Komponenten der App müssen in vielen Fällen miteinander kommunizieren, um sich zum Beispiel gegenseitig mit Events und Updates zu versorgen.

Ein einfaches Beispiel dafür sind Push-Nachrichten. Angenommen die App empfängt über einen BroadcastReceiver einen Push und liefert diesen in einen Hintergrundprozess der App weiter (in der Regel ein IntentService). Jetzt steht man vor der Aufgabe, wie man von dieser Stelle aus genau die Teile der App informiert, für die der Empfang des Pushes von Bedeutung ist. Zum Beispiel weil man im User Interface durch ein grafisches Element andeuten will, dass eine neue Nachricht angekommen ist oder man den Datenbank-Layer auffordern möchste, die Daten des Pushes zu speichern.

Eine Möglichkeit dieses Problem zu lösen sind normale Referenzen auf die jeweiligen Fragments oder Activities. Aber will man das? Eher nicht. Man würde damit feste Beziehungen zwischen diesen Komponenten eingehen und ein späteter Austausch von Teilen der App wäre mehr als umständlich. Von der Null-Pointer-Gefahr mal ganz zu schweigen.

Android ohne Event Bus

Android ohne Event Bus und mit festen Beziehungen zwischen den Komponenten

Android selbst bringt von Haus aus ein Event-System mit, das als solches vielleicht zunächst gar nicht zu erkennen ist: Android bietet die Möglichkeit Activities über Intent-Extras und Fragments über Bundles mit den entsprechenden Daten zu starten. Aber auch hier ist man nicht wirklich flexibel: Man muss auch diese Activities & Fragments wieder fest verdrahten und ist auch etwas eingeschränkt bei der Wahl der Datentypen.

Und genau an dieser Stelle kommt der Event Bus ins Spiel. Ein Event Bus ist quasi nichts anderes als ein Kommunikationskanal, in den man zum einen reinhören und zum anderen reinrufen kann. Am Beispiel von oben erklärt: Eine Komponente der App, die wartet bis ein Push ankommt, horcht in den Bus hinein. Der Receiver, der den Push empfängt, ruft bei Empfang die jeweiligen Infos in den Bus. Sprich wir haben eine klassische Publish/Subscriber-Architektur, ähnlich den Listenern, aber generischer zu nutzen.

Android mit Event Bus

Android mit Event Bus. Loose Coupling und saubere interne Kommunikation

GreenRobot, Otto und Guava

Für Android gibt es drei Frameworks um mit einem Event Bus zu arbeiten. Guava von Google ist ein EventBus für Java, der auch, aber nicht nur, für Android genutzt werden kann. Der GreenRobot EventBus und Square’s Otto sind speziell für Android angepasste Variante von EventBus Systemen.

Die Frage, ob man Otto oder den GreenRobot einsetzt, ist teilweise mehr ein Glaubenskrieg als ein faktisches Abwegen. GreenRobot wirbt zwar mit vielen Features, die Otto nicht unterstützt, teilweise sind diese aber auch aus Gründen der Einfachheit nicht in Otto enthalten (z.B. lässt sich die Delivery der Events von Background Threads in den Main Thread mit recht wenig Code auch hier verwirklichen).

Was man nun verwendet, ist unserer Meinung nach Geschmackssache. Wir arbeiten sehr gerne mit Otto, da die einfache Verwendung in Kombination mit dem guten Ruf, den die Android Bibliotheken von Square haben, uns immer wieder überzeugt haben. Ich bin mir aber relativ sicher, dass wir genauso mit Greenrobot glücklich geworden wären.

Beispiel: Ein EventBus mit Otto

Greifen wir das Beispiel von oben auf und implementieren beispielhaft einen EventBus, der in einem Fragment generierte Events an die darunter liegende Activity schickt.

Es bietet sich an, den EventBus in einem Singleton bereit zu stellen, um nicht bei jeder Verwendung das Objekt neu zu initialisieren.

public class CortexBus {
    private static Bus instance;	

    private CortexBus() {}

    public synchronized static Bus getInstance() {
        if (instance == null) {
            instance = new Bus();
        }
        return instance;		
    }
}

Im Fragment wollen wir die Events verschicken. Als Beispiel nehmen wir eine Kurznachricht, die dieses Fragment z.B. über eine HTTP Schnittstelle empfangen hat. Hier sind beliebig komplexe Events möglich, da jeder Event über eine eigenen Klasse (ein einfaches POJO) repräsentiert wird. Diese sieht in unserem Beispiel wie folgt aus:

public class MessageReceivedEvent {	
    public long messageId;
    public String message;

    public MessageReceivedEvent(long messageId, String message) {
        this.messageId = messageId;
        this.message = message;
    }
}

Diesen Event können wir nun über den Bus an alle registierten (siehe unten) Subscriber schicken, ohne diese direkt kennen zu müssen:

public class CardFragment extends Fragment {
    private CortexBus bus;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        bus = CortexBus.getInstance();

        // code, um die nachricht zu empfangen, usw.
        // long messageId = ...
        // String message = ...

       bus.post(new MessageReceivedEvent(messageId, message));

        // ... weiterer Code ...
    }
}

In der Activity wollen wir auf eingehende Events hören. Wir initialisieren den Bus über den Singleton und erstellen eine Methode mit der @Subscribe Annotation, die die jeweiligen Events empfängt. Welchen Event diese Methode empfängt, wird über die Methodensignatur und die Annotation entschieden. Der Name der Methode ist hierbei nicht wichtig und kann frei gewählt werden. Die Activity muss sich dazu im onResume() und onPause() jeweils an- bzw. abmelden, damit die Events überhaupt empfangen werden.

public class MainActivity extends FragmentActivity {
    private Bus bus;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        bus = CortexBus.getInstance();
    }

    @Override
    protected void onResume() {
        super.onResume();		
        bus.register(this);
    }		

    @Override
    protected void onPause() {
        super.onPause();
        bus.unregister(this);
    }

    @Subscribe 
    public void onPushReceived(PushReceivedEvent event) {
        // dein event handling passiert an dieser Stelle
        // ....
    }
}

Das ist alles, was für ein einfaches Event Bus System nötig ist. Man erreicht damit ein sehr angenehmes und stabiles Loose Coupling der Komponenten seiner App. Vor allem wenn die eigene Anwendung komplexer wird, kann man sich damit sehr viel Ärger und NPE-Schmerz ersparen.

In Technischer Hintergrund veröffentlicht | Getaggt: ,

X-Sendfile zur Sicherung von Userinhalten

Für viele unserer Kunden ist die Datensicherheit ihr höchstes Gut: Dabei geht es aber nicht nur um den Schutz ihrer Daten vor fremdem Zugriff von außen, sondern auch um die Sicherheit der Daten einzelner User untereinander. Seien es hochgeladene Texte, Profilbilder oder gar Lebensläufe, es muss garantiert werden, dass nur berechtigte User diese Daten sehen dürfen.

Leider ist dies bei vielen Webseiten nicht der Fall. Zumeist liegen alle hochgeladenen Dateien öffentlich zugänglich in einem Unterordner des htdocs-Verzeichnisses und der einzige Schutz, der vor einem unerlaubten Zugriff existiert ist die Tatsache, dass die Dateinamen dem Aufrufer unbekannt sind.
Dies ist natürlich nur ein marginaler Schutz, insbesondere weil man sich aus eigenem Interesse bemühen sollte, die Dateien sinnvoll und ordentlich zu benennen.

Die einfachste Lösungsmöglichkeit für dieses Problem ist es nun, die Dateien außerhalb des htdocs-Verzeichnisses zu speichern und dann mit Hilfe einer Skriptsprache wie PHP als Stream auszuliefern. Das folgende Skript demonstriert dies beispielhaft:

<?php
    //Achtung, im Produktiveinsatz sollte hier noch überprüft werden ob der Benutzer berechtigt ist
    //die Datei abzurufen
    if(!empty($_GET['file'])){
        //$config ist z.B. ein Objekt welches die Konfiguration der Seite gespeichert hält
        $file = $config->getFilePath().$_GET['file'];
        if(file_exists($file)){
            header ('Content-Type: application/octet-stream');
            header ('Content-Length: '.filesize($file));
            readfile($file);
        }
     }
?>

Diese Methode hat jedoch drei große Nachteile:

  1. Die Methode wirkt sich negativ auf die Performance aus, da die Datei nicht direkt vom Webserver übertragen wird, sondern ein Prozess der Skriptsprache solange läuft, bis die Datei komplett gestreamt wurde. Gerade bei hochfrequentierten Seiten kann die Ausführung dieser vielen einzelnen Skript-Prozesse die Performance der Seite sehr belasten, da die Prozesse von solchen Skripten einen unnötigen Speicherbedarf erzeugen.
  2. Viele Webserver beschränken (sinnvollerweise) die Ausführungszeit der Aufrufe von Skripten (z.B. auf 60 Sekunden). Bei größeren Dateien würde dies unweigerlich zum Abbruch des Downloads führen.
  3. Im Normalfall werden vom Webserver Skriptaufrufe nicht gecacht, da es sich ja um dynamische Inhalte handelt. Bei der Auslieferung von Dateien wollen wir aber natürlich den Vorteil des Cachings nur ungern verlieren, wie z.B. über E-Tags oder conditional GETs.

Übertragung mit X-Sendfile – Die bessere Variante

Die Apache2 Mod X-Sendfile bietet eine elegantere Möglichkeit, das obige Konzept der Auslagerung der Dateien außerhalb des htdocs-Verzeichnisses umzusetzen. Die Idee dieser Mod ist einfach: Anstatt dem Server nur die Inhalte des fertig ausgelieferten Skripts zu übergeben, wird dem Skript ermöglicht mit dem Webserver zu „kommunizieren“. Das geschieht, indem Apache die Skriptdatei nach dem gesetzen X-Sendfile Header durchsucht. Sollte dieser gefunden werden, wird statt dem Skript die im X-Sendfile Header angegebene Datei übertragen. Das folgende Skript demonstriert die obige Dateiauslieferung, diesmal mit X-Sendfile umgesetzt:

<?php
    //Achtung, im Produktiveinsatz sollte hier noch überprüft werden ob der Benutzer berechtigt ist
    //die Datei abzurufen
    if(!empty($_GET['file'])){
        //$config ist z.B. ein Objekt welches die Konfiguration der Seite gespeichert hält
        $file = $config->getFilePath().$_GET['file'];
        if(file_exists($file)){
            header('X-Sendfile: '.$file);
            header ('Content-Type: application/octet-stream');
            header ('Content-Disposition: attachment; filename='.$file );
            exit;
        }
     }
?>

Zugangsschutz mit X-Sendfile

Nachdem wir nun eine Möglichkeit gefunden haben, unsere Dateien vor einem direkten Zugriff über den Dateinamen zu schützen, können wir unsere Dateien effektiv vor einem Fremdzugriff sichern. Als ein einfaches Beispiel nehmen wir an, dass unsere Webseite die Lebensläufe ihrer User ausliefern soll und zwar nur an den Besitzer des Lebenslaufs selbst oder an eine Firma bei der sich der User beworben hat. Wir haben die Lebensläufe schon außerhalb des htdocs-Ordners gespeichert und wollen diese nun über die Datei get_cv.php ausliefern.

Unser HTML Link würde folgendermaßen aussehen:

<html>
    ...
    <body>
        ...
        <p>
             <a href="get_cv.php?userid=10">Lebenslauf herunterladen</a>
        </p>
    </body>
</html>

Die PHP Datei selbst überprüft nun zunächst ob der Benutzer der Besitzer des Lebenslaufs ist, oder ob er sich bei der Firma, die den Link aufruft, beworben hat. Aus Gründen der Übersichtlichkeit gehen wir davon aus, dass einige Objekte und Methoden an anderer Stelle implementiert wurden.

<?php
    if(!empty($_GET['userid'])){
         //Der Usermanager verwaltet alle User unseres Portals
         $user = $usermanager->getUser(intval($_GET['userid']));
         $loggedInUser = $usermanager->getCurrentLoggedInUser();

         //Der Lebenslauf gehört uns selbst, wir dürfen ihn downloaden
         if($user->getId() == $loggedInUser->getId()){
            header('X-Sendfile: '.$loggedInUser->getCV());
            header ('Content-Type: application/octet-stream');
            header ('Content-Disposition: attachment; filename='.$loggedInUser->getCV() );          
         }
         //Der Besitzer des Lebenslaufs hat sich bei der Firma die im Moment eingeloggt ist beworben
         else if($loggedInUser->isCompany() && $loggedInUser->userAppliedForJob($user)){
            header('X-Sendfile: '.$loggedInUser->getCV());
            header ('Content-Type: application/octet-stream');
            header ('Content-Disposition: attachment; filename='.$loggedInUser->getCV() );
         }else{
              die("Es wurde keine User-ID übergeben.");
         }
    }else{
        die("Es wurde keine User-ID übergeben.");
    }
?>

Mit diesem einfachen Skript haben wir unsere Dateien vor unbefugtem Zugriff geschützt und können uns beliebig komplexe Zugriffsregeln ausdenken und in PHP umsetzen. Natürlich bieten sich einem auch noch viele weitere Möglichkeiten durch die Vorschaltung eines PHP Skripts, exemplarisch seien hier genannt:

  • Abrufen von Informationen aus der Datenbank, z.B. um den Dateiennamen anzupassen
  • Speichern von Daten beim Aufruf, z.B. ein Downloadzähler
  • Bezahlung der Inhalte vor dem Ausliefern der Dateien

Sicherheitsbedenken

Zum Abschluss möchte ich noch auf eine Gefahr hinweisen, die durch dein Einsatz von X-Sendfile enstehen kann. Da der Webserver jede Datei, die im X-Sendfile Header angegeben wird und auf die er lesend zugreifen darf, auch ausliefert, muss der Programmierer sehr genau aufpassen, dass er keine große Sicherheitslücke in seinem Server öffnet.

Dies ist insbesondere dann der Fall, wenn der Benutzer die gewünschte Datei z.B. über einen GET-Parameter an das PHP Skript übergibt. In diesem Fall kann der Angreifer über dein Einsatz des ..-Operators auch auf tiefere Verzeichnisse des Dateisystems zugreifen.

Es ist also zu empfehlen entweder dem Benutzer nicht direkt die Möglichkeit zu geben, den Dateinamen anzugeben, oder zumindest den übergebenen Dateinamen so zu bereinigen, dass kein Schaden enstehen kann. Dies ist zum Beispiel sehr einfach mit str_replace möglich:

<?php
    str_replace("..","",$filename);
?>

Wenn X-Sendfile jedoch richtig und sicher eingesetzt wird, erhöht es sowohl die Sicherheit wie auch Performance eines Webportals um ein Vielfaches.

In Technischer Hintergrund veröffentlicht | Getaggt: , , , ,

SSL Pinning und Android

Wenn Apps über das Internet kommunizieren, verschlüsseln heute alle seriösen App-Entwickler ihre Daten per SSL (das war nicht immer so). Und das ist sehr sinnvoll, nicht nur um Zuge der NSA-Enthüllungen der letzten Monate. Niemand möchte, dass beispielsweise die eigenen Google-Suchanfragen für jeden im freien WLAN des Cafés nebenan mitlesbar sind. Nicht selten sind diese äußerst privater Natur.

Trotzdem sind viele kleine Details zu beachten, damit SSL sinnvoll und nicht falsch angewandt wird. Und hier spielt SSL Pinning eine wichtige Rolle.

SSL Pinning

Mit SSL Pinning gibt man dem Client (in unserem Fall ist dies die App) fest vor, welches Zertifikat er auf dem Server zu erwarten hat. Findet er ein anderes Zertifikat vor, wird die Verbindung nicht akzeptiert. Ein großer Vorteil bei Entwicklung von Apps ist der Fakt, dass man wieder Kontrolle über den Client bekommt, und nicht nur über den Server. So kann man im Client das auf dem Server zu erwartende Zertifikat fest definieren und damit jedes andere (böse) Zertifikat ausschließen. Bei Webapplikationen ist die Kontrolle des Browsers des Users je nach Kundenstamm deutlich schwerer bis unmöglich.

SSL Pinning bringt nun gleich mehrere Vorteile:

  • Man-In-The-Middle-Angriffe sind nicht möglich, da dem Nutzer gar kein falsches Zertifikat untergeschoben werden kann. Es wird im Code, und nicht über eine Auswahl des Users, genau ein gegebenes Zertifikat akzeptiert. Andere Zertifikate werden abgelehnt.
  • Probleme mit kompromittierten Root-Zertifikate sind ausgeschlossen (was schon öfter der Fall war, als man annehmen sollte).
  • Angreifer können (ihren eigenen) Traffic zwischen der App nicht analysieren, um damit eventuell bisher unbekannte Lücken zu finden oder mit gefälschten Anfragen Exploits zu finden. Mit Tools wie dem mitmproxy sind Analysen dieser Art in Browsern sehr einfach durchzuführen.

SSL Pinning für Android

Für Android Apps ist SSL Pinning recht einfach umzusetzen. Hier eignet sich das exzellente Library Project AndroidPinning von Moxie Marlinspike. Dieser Weg ist vor allem dann interessant, wenn man auf ein von einer CA signiertes Zertifikat angewiesen ist, weil man z.B. ein gemeinsames Zertifikat mit dem Webfrontend teilt. Ohne diesen Constraint wäre es natürlich auch möglich, komplett auf CA-signierte Zertifikate zu verzichten und eigene Zertifikate zu erstellen und die CA Chain damit komplett zu umgehen. Dieser Weg mag sogar der sicherere sein, ist aber nicht immer praktikabel.

Wir clonen zuerst die Library via git:

git clone https://github.com/moxie0/AndroidPinning

und binden das Library Project wie gewohnt in unsere App ein. Hilfe bieten hier auch die Android Developer Docs.

Dann benötigen wir eine Information im Zertifikat, die wir pinnen können. Hier eignet sich die SubjectPublicKeyInfom, die wir wie folgt aus dem entsprechenden Zertifikat extrahieren:

$ tools/pin.py dein-cert.pem
Calculating PIN for certificate: O=api.cortex-media.de, OU=Go to https://www.thawte.com/repository/index.html, OU=Thawte SSL123 certificate, OU=Domain Validated, CN=api.cortex-media.de
Pin Value: 425c3f8c27c44ab1e0818812cb7115e30140a31b

(oder alternative auch eine .crt Datei, je nachdem was vorliegt.)

Der PIN Value ist die Informationen, die wir in den nachfolgenden Schritten in der Android App hart verdrahten. Im unteren Beispiel nutzen wir die „425c3f8c27c44ab1e0818812cb7115e30140a31b“ als Beispiel für eine PIN.

Wenn für die HTTP Kommunikation via HttpClient gearbeitet wird, kann der Client wie folgt initialisiert werden:

String[] pins          = new String[] {"f30012bbc18c231ac1a44b788e410ce754182513"};
HttpClient httpClient  = PinningHelper.getPinnedHttpClient(context, pins);
HttpResponse response  = httpClient.execute(new HttpGet("https://www.google.com/"));

Für die HttpsUrlConnection läuft die Initialisierung nahezu analog ab:

String[] pins          = new String[] {"425c3f8c27c44ab1e0818812cb7115e30140a31b"};
URL url                = new URL("https://www.google.com");
HttpsURLConnection con = PinningHelper.getPinnedHttpsURLConnection(context, pins, url);

Und das wäre dann auch schon alles: Kein anderes Zertifikat als angegebene wird akzeptiert und die SSL Kommunikation ist ein Stück weit sicherer.

In Technischer Hintergrund veröffentlicht | Getaggt: ,

Die perfekte SSL Cipher Suite für Apache (und andere)

„SSL ist nicht mehr sicher!“ konnte man in den letzten Wochen das ein oder andere mal in den einschlägigen Nachrichtenmagazinen lesen. Aber stimmt das? Wir denken nicht.

Es ist korrekt, dass manche der von SSL verwendeten Verschlüsselungsalgorithmen nicht (mehr) stark genug sind (z.B. RC4) oder als von der NSA kompromittiert vermutet werden. Aber das gilt nicht für alle. Vor allem nicht für die mathematischen Grundlagen, die hinter den Algorithmen stecken.

Wer einen Apache Webserver mit SSL betreibt, und unter allen Cipher-Suiten die schwarzen Schafe ausschließen, Perfect Forward Secrecy aktivieren und damit deutlich an Sicherheit gewinnen will, kann sich mit der folgenden Direktive von allen Algorithmen verabschieden, die als nicht (mehr) sicher gelten.

Für den Apache sieht dies wie folgt aus, für Nginx analog, da die Syntax für die Auswahl der Suiten analog läuft.

SSLEngine On
SSLProtocol all -SSLv2
SSLHonorCipherOrder On
SSLCipherSuite EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA256:EECDH+aRSA+RC4:EDH+aRSA:EECDH:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS
In Technischer Hintergrund veröffentlicht | Getaggt: ,

Fortschrittliches Testing von iPhone Apps mit UIAutomation

Das gründliche Testen von iPhone Apps kann eine große Herausforderung sein. Besonders durch den kurzen Update Zyklus, sowohl von Entwicklerseite (regelmäßige Updates der eigenen App) wie auch von Apple (Veröffentlichung neuer iOS Versionen mit teils großen Änderungen an der UI) werden notwendige Tests oft zu einer sehr mühsamen und repetitiven Aufgabe.

Mit UIAutomation hat Apple deshalb ab iOS 4.0 eine Möglichkeit geschaffen, automatisierte Tests durchzuführen und dem Entwickler somit viel Arbeit zu sparen. Der Grundgedanke von UIAutomation ist dabei, bei den Tests tatsächlich die UI der App anzusprechen (d.h. es werden tatsächliche Touches simuliert), und nicht wie in klassischen Testverfahren Methoden zu schreiben, welche nur den Sourcode der App ansprechen. Es gibt somit keinen Bruch zwischen den Testfällen und der späteren tatsächlichen Bedienung durch den Benutzer.

UIAutomation ist Teil des Instruments Sets von XCode und kann sowohl Apps auf dem iPhone, iPad aber auch im iPhone Simulator automatisch bedienen. Die Tests selbst werden dabei in Javascript geschrieben und greifen auf die Accessibility Eigenschaften der UI Elemente der App zu.

Im Folgenden werden wir anhand eines kleines Testprogramms eine kurze Einführung in UIAutomation geben und demonstrieren, warum wir bei fast allen unseren Apps im Testing darauf bauen.

Erstes Beispiel in UIAutomation anhand einer Beispiel App

Erster TableViewControllerZweiter TableViewController

Für unser Beispiel haben wir eine rudimentäre Fussballverwaltungs-App implementiert. Sie besteht aus zwei TableViewController, welche einmal die angelegten Vereine und einmal die angelegten Spieler darstellen. Zwischen den Controllern kann über eine TabBar gewechselt werden. Außerdem kann man in der App einen Verein hinzufügen und Spieler löschen.

Erstellen eines neuen Skripts in Instruments

Erstellen eines neuen Skripts in Instruments

Als erstes wollen wir automatisch in das Tab „Spieler“ wechseln und dort an eine bestimmte Position scrollen. Dazu erstellen wir ein neues Skript (siehe Screenshot) und geben folgenden Code ein:

//Um Tipparbeit zu sparen, definieren wir uns hier zwei Hilfsvariablen
var target = UIATarget.localTarget();
var app = target.frontMostApp();

/*
* Es ist zu empfehlen, einzelne Tests in Funktionen auszulagern. 
* Dies steigert die Übersichtlichkeit und ermöglicht es einzelne Tests seperat durchzuführen
*/
function scrollToPlayer(){
    app.tabBar().buttons()["Spieler"].tap();
    app.mainWindow().tableViews()[0].scrollToElementWithPredicate("name beginswith 'Oliver Baumann'");
}

//An dieser Stellen rufen wir unsere definierte Testfunktion auf
scrollToPlayer();

Jedes UIKit Element der App wird in Javascript durch ein zugehöriges UIAElement repräsentiert und findet sich, je nach Position in der jeweiligen View, auf einer gewissen Position in der View Hierarchie wieder. Wenn auf ein bestimmtes Element zugegriffen werden soll, muss in der View Hierarchie zum entsprechenden Element hin abgestiegen werden. In unserem Beispiel greifen wir zum Beispiel auf die TabBar unser App zu, holen uns aus der View Hierarchie alle Schaltflächen der TabBar und weisen UIAutomation an, auf den Button mit der Bezeichnung „Spieler“ zu klicken.

Beispielhafte View-Hierarchie

Beispielhafte View-Hierarchie

Genauso selektieren wir die erste (und in diesem Fall einzige) TableView unserer View Hierarchie und weisen UIAutomation an, zu einer bestimmten Zelle dieser Tabelle zu scrollen. Wir können diese Zelle mit einem einfachen Predicate auswählen oder auch direkt bestimmen, zu welcher Zelle gescrollt werden soll.

Sinnvolles Logging und das Definieren von Testfällen

In unserem bisherigen Beispiel musste der Benutzer selbst am Handy überprüfen, ob das Gerät die gegebenen Anweisungen richtig ausgeführt. Dies natürlich nicht die Idee von automatischen Tests. Stattdessen wollen wir nun im nächsten Schritt Testfälle definieren, Logging-Ausgaben einführen und automatisch Screenshots anfertigen.

Als unseren Testfall möchten wir dazu einen neuen Verein zur Liste hinzufügen. Dazu klicken wir zunächst auf den + Knopf in der Vereinsübersicht, tragen dann den Namen des neuen Vereins in das Textfeld ein, klicken auf Speichern und überprüfen danach ob die neue Zelle in der Tabelle vorhanden ist.

In diesem Bildschirm kann ein Verein hinzugefügt werden.

In diesem Bildschirm kann ein Verein hinzugefügt werden.

Im automatisierten Javascript-Code sieht das folgendermaßen aus:

var target = UIATarget.localTarget();
var app = target.frontMostApp();

function addClub(clubName){
    //Hiermit wird ein neuer Testfall angelegt und benannt
    UIALogger.logStart("Anlegen eines neues Vereins: "+clubName);

    //Wir navigieren auf die Anlege-Seite und füllen das Formular aus
    app.navigationBar().buttons()["Add"].tap();
    app.mainWindow().textFields()[0].setValue(clubName);
    app.mainWindow().buttons()["Speichern"].tap();

    //Wir loggen das erfolgreichen Ausfüllen des Formulars
    UIALogger.logMessage("Formular erfolgreich ausgefüllt");

    //Wir selektieren die (hoffentlich) neu angelegte Zelle
    var cell = app.mainWindow().tableViews()[0].cells().firstWithPredicate("name beginswith '"+clubName+"'");

    //Wir warten kurz bis die Übergangsanimation zwischen den Screenshots beendet ist
    target.delay(1);

    //Wir machen einen Screenshot der TableView um zu sehen ob unser unteres Ergebnis auch stimmt
    target.captureScreenWithName("Tabelle nach Anlegen");

    //Eine Zelle ist dann valid, wenn Sie in der TableView exisiert und somit wurde auch unser Test bestanden
    if(cell.isValid()){
        UIALogger.logPass("Anlegen eines neues Vereins: "+clubName);
    }else{
        UIALogger.logFail("Anlegen eines neues Vereins: "+clubName);	
    }
}

addClub("SC Freiburg");

Wir haben nun also einen Testfall definiert und können beim Ausführen des Skripts in Instruments den Test verfolgen und sehen, ob der Test erfolgreich war. Zu dem können wir uns im Nachhinein alle Testfälle mit Log-Ausgaben und Screenshots anschauen und so etwaige Fehler untersuchen.

Erfolgreiche Tests können in Instruments nachvollzogen werden.

Erfolgreiche Tests können in Instruments nachvollzogen werden.

Mit diesen Instrumenten ist uns nun möglich ein Set von Tests zu definieren, welche alle erfolgreich durchlaufen werden müssen, bevor wir ein Update der App freigeben. Ein Testen der App auch nach kleinen Änderungen ist somit ohne weiteren Aufwand möglich.

Kompliziertere Aktionen

Natürlich haben wir in unseren bisherigen Beispielen nur an der Oberfläche der Fähigkeiten von UIAutomation gekratzt. Einige besondere Eigenschaften und Fähigkeiten möchten ich an dieser Stelle noch hervorheben, da in diesen Fällen nochmals die Überlegenheit von UIAutomation zu handgeschriebenen Tests klar wird:

Behandlung von Alerts
Ein großes Problem bei händischen bzw. automatischen Tests ohne UIAutomation sind zufällig auftretende Alerts (z.B. weil das Handy einen Anruf oder eine SMS empfängt). UIAutomation kann diese Alerts automatisch schließen und fährt danach an der richtige Stelle mit dem Test fort. Der Test ist also nicht automatisch unbrauchbar, nur weil ein Alert aufgetreten ist. Dies ist besonders für zeitintensive automatische Tests sehr praktisch.

UIATarget.onAlert = function onAlert(alert){
    UIALogger.logWarning("Der Alert '" + alert.name() + "' hat die Ausführung unterbrochen!");
    return false; // use default handler
}

Zu dem können eigene Alert Handler programmiert werden, um mit auftretenden Alerts umzugehen. Zum Beispiel wenn ein Verein doppelt in die Liste eingetragen wird, könnte ein Alert von der App gefeuert werden und UIAutomation in einem solchen Alert-Handler dieses Verhalten behandeln.

Multitasking Tests
Mit der Einführung des Multitaskings und dem damit möglichen Wechsel zwischen den Apps sind einige Herausforderungen beim Testen hinzugekommen, da gestestet werden muss, ob die App auch den jeweiligen Screen bzw. die jeweilige Funktion nach dem kurzzeitigen Wechseln zwischen den Apps noch richtig ausführt. UIAutomation bietet hier eine einfache Möglichkeit Multitasking durch den Benutzer zu simulieren:

UIATarget.localTarget().deactivateAppForDuration(5);

Diese Zeile schickt die App für 5 Sekunden in den Hintergrund und fährt danach mit den Tests des programmierten Skripts fort.

Ändern der App-Orientation
UIAutomation kann auch die Rotation des iPhones simulieren und somit z.B. zwischen Landscape und Portrait Modus hin- und herschalten. Zu beachten ist dabei jedoch, dass die Bewegungssensoren des iPhones dabei nicht angesteuert werden, sondern das Drehen nur simuliert ist. Wenn man also auf Bewegungssensordaten angewiesen ist, reicht diese Methode für einen Test nicht aus.

var target = UIATarget.localTarget();
var app = target.frontMostApp();

target.setDeviceOrientation(UIA_DEVICE_ORIENTATION_LANDSCAPELEFT);
UIALogger.logMessage("Das Telefon befindet sich im " + app.interfaceOrientation() + "Modus.");
target.setDeviceOrientation(UIA_DEVICE_ORIENTATION_PORTRAIT);

Multi-Touch
Bis jetzt haben wir nur einfach Taps auf dem Display ausgeführt. Viele Apps benutzen aber auch komplizierte Multi-Touch gesten zur Bedienung, und sei nur ein Swipe um einen Tabelleneintrag zu löschen. Natürlich bietet UIAutomation auch die Möglichkeit diese komplizierteren Gesten zu simulieren.
Zum Beispiel ist möglich die zwei Finger Zoom-Geste (sog. Pinch-Geste) über folgenden Aufruf durchzuführen:

UIATarget.localTarget().pinchOpenFromToForDuration({x:20, y:200}, {x:300, y:200},2);

Und eine Flick-Geste (zum Beispiel um ein Swip-To-Delete aufzurufen) wird folgendermaßen ausgeführt:

UIATarget.localTarget().flickFromTo({x:160, y:200}, {x:160, y:400});

Delay – Wenn’s mal länger dauert
Ein häufiges Problem beim Einsatz von UIAutomation ist die Einbeziehung von Wartezeiten, typischerweise weil eine Animation beendet werden soll oder gewartet werden muss, bis Daten aus dem Internet heruntergeladen worden sind. Ein einfach Hilfsmittel ist es hier, den Test einfach kurz warten zu lassen:

var target = UIATarget.localTarget();
target.delay(1);

Der Test wartet nun eine Sekunde, bevor er mit der weiteren Ausführung fortfährt.

Zusammenfassung und weiterführende Links

UIAutomation ist ein sehr mächtiges Werkzeug, welches das Testen von iPhone Apps unglaublich vereinfacht und auf eine neue Ebene gehoben hat. Es ist für jede halbwegs komplexe App zu Empfehlen Testfälle zu definieren und die Tests vor jedem Update durchlaufen zu lassen. Mit den Logging und Screenshot Funktionen ist auch eine Fehlersuche relativ intuitiv möglich.

Mehr über das UIAutomation findet man zum Einen natürlich bei Apple:

Aber auch in in einigen empfehlenswerten Blogs:

In Unsere Projekte veröffentlicht | Getaggt: , , , , ,