Größere Dateien tauscht die Jugend heute gerne über den proprietären Dropbox-Service aus. Dessen Web-API erlaubt auch den Einsatz selbst geschriebener Skripte, beispielsweise zum Abholen einer Datei aus dem Schatten einer Firewall.
Neulich wollte mir eine Praktikantin ein digitales Hörbuch leihen und schlug vor, es mir zu “dropboxen”. Dass der unter Dropbox.com erhältliche Service bereits zum Standard für den Austausch größerer Dateien avanciert ist, war mir neu. Auch dass die amerikanische Jugend dropboxen bereits als Verb wie googeln verwendet, gab mir zu denken.
Von der Website auf http://dropbox.com lädt der User kostenlos ein Clientbinary herunter (Windows, Mac und sogar Linux werden unterstützt) und sieht dann auf seinem System einen neuen lokalen Dropbox-Folder. Schiebt er Dateien hinein, springt hinter den Kulissen eine magische Software an, die den neuen Inhalt unauffällig und tröpfchenweise auf den Dropbox-Server hochlädt.
Von dort synchronisieren sich dann weitere Clients desselben Users (oder die von extra autorisierten Freunden) ebenso magisch, sodass ein User auf jedem Computer der Welt einen permanent aktualisierten Order mit seinen Dateien findet. Falls irgendwo kein Dropbox-Client installiert ist, benötigt der User lediglich einen Browser und geht auf die Dropbox-Website (siehe Abbildung 1).

Abbildung 1: Das Webinterface von Dropbox ermöglicht die Manipulation von Dateien auch auf solchen Rechnern, auf denen kein Client installiert ist.
Open Source bevorzugt
Allerdings behagte mir der Gedanke nicht, ein Binary ohne einsehbaren Sourcecode auf meinem PC zu Hause zu starten. Zum Glück bietet Dropbox aber auch ein Web-API an, mit dem ein paranoider Pinguinfreund wie ich seine helle Freude an dem kostenlosen Service haben kann. Vernünftigerweise möchte Dropbox.com auch die User von API-getriebenen Programmen dazu erziehen, ihren Usernamen und ihr Passwort nicht auf irgendwelchen Oberflächen von Drittanbietern einzutippen, und setzt deswegen auf Oauth. Um den User bei der Applikation anzumelden, holt die zunächst von der Dropbox-Website unter Angabe eines Developer-Token und -Secret einen Request-Token mit -Secret ab. Mit dem Request-Token in der URL lenkt dann die Applikation den Browser des Users zur Dropbox-Website, die ihn, falls er dort noch nicht eingeloggt ist, zur Angabe seines Usernamens mit Passwort auffordert (Abbildung 2).

Abbildung 2: Die Perl-Applikation verweist auf Dropbox.com, wo der User seine Kenndaten eingibt, um sich zu autorisieren.
Klappt das Login, fragt Dropbox den User, ob er wirklich damit einverstanden ist, dass die Applikation »Perl Test Client« Zugriff auf seine Dropbox-Daten erhält (siehe Abbildung 3). Bestätigt er dies, lenkt die Dropbox-Website den Browser wieder zurück zur Applikation und schickt in der URL einen so genannten Access-Token mit. Den darf die Applikation dann speichern und damit bis zum Verfallsdatum die Aufträge des Users unter dessen Dropbox-Account ausführen.

Abbildung 3: Der User bestätigt, dass er der Perl-Applikation die Verwaltung seiner Dropbox-Daten anvertraut.
Webserver als Gerüstbau
Da ein Perl-Client als Kommandozeilen-Applikation normalerweise kein Browserinterface betreibt, klopft das Skript »dropbox-init« in Listing 1 mit dem Modul Mojolicious::Lite vom CPAN schnell einen notdürftigen Webserver auf http://localhost:8082 zusammen. Er reagiert auf die Pfade »/« sowie »/callback« und betreibt damit eine Startseite und eine Rücksprungadresse nach erfolgreicher Anmeldung des Users auf der Dropbox-Website. Der HTML-Code, den der Server jeweils ausspuckt, liegt im »__DATA__« -Segment ab Zeile 64. Mojolicious sucht dort wegen des Verweises auf »index« in Zeile 37 nach »@@ index.html.ep« und sendet das dort liegende HTML zurück, nachdem es die Template-Variablen ersetzt hat. Die Anweisung »% layout ‘default’;« setzt das Drumherum, damit aus der Meldung auch ein wohlgeformtes HTML-Dokument wird.
Listing 1
dropbox-init
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Mojolicious::Lite;
04 use Net::Dropbox::API;
05 use YAML qw(LoadFile DumpFile);
06
07 my $dev_key = "iyaiu823ajksgwf";
08 my $dev_secret = "zlkj32lkj2kl3dp";
09 my $listen = "http://localhost:8082";
10 my($home) = glob '~';
11 my $CFG_FILE = "$home/.dropbox.yml";
12
13 my $CFG = {};
14 $CFG = LoadFile( $CFG_FILE ) if
15 -f $CFG_FILE;
16
17 @ARGV = (qw(daemon --listen), $listen);
18
19 my $box = Net::Dropbox::API->new({
20 key => $dev_key,
21 secret => $dev_secret,
22 });
23
24 my $REQUEST_TOKEN;
25 my $REQUEST_SECRET;
26
27 ###########################################
28 get '/' => sub {
29 ###########################################
30 my ( $self ) = @_;
31
32 $box->callback_url( "$listen/callback" );
33 $self->stash->{login_url} = $box->login;
34
35 $REQUEST_TOKEN = $box->request_token;
36 $REQUEST_SECRET = $box->request_secret;
37 } => 'index';
38
39 ###########################################
40 get '/callback' => sub {
41 ###########################################
42 my ( $self ) = @_;
43
44 $box->auth({
45 request_token =>
46 $self->param('oauth_token'),
47 request_secret =>
48 $REQUEST_SECRET
49 });
50
51 $CFG->{ access_token } =
52 $box->access_token();
53 $CFG->{ access_secret } =
54 $box->access_secret();
55
56 DumpFile $CFG_FILE, $CFG;
57
58 $self->render_text( "Token saved.",
59 layout => 'default' );
60 };
61
62 app->start;
63
64 __DATA__
65 ###########################################
66 @@ index.html.ep
67 % layout 'default';
68 <a href="http://<%=%20$login_url%20%>"
69 >Login on dropbox.com</a>
70
71 @@ layouts/default.html.ep
72 <!doctype html><html>
73 <head><title>Token Fetcher</title></head>
74 <body>
75 <pre>
76 <%== content %>
77 </pre>
78 </body>
79 </html>
Startet der User die Mojolicious-Applikation und tippt http://localhost:8082 in den Browser, erscheint das notdürftige UI in Abbildung 4 mit einem Link auf die Login-Seite von Dropbox.com.
Das Modul Net::Dropbox::API, ebenfalls vom CPAN, abstrahiert schön die Dropbox-Zugriffe und die Oauth-Autorisierung [2]. Setzt der API-Developer im Konstruktoraufruf in Zeile 19 die auf der Dropbox-Developer-Site [3] eingeholte Kombination aus Developer Key und Secret, greift der Aufruf der Methode »login()« (Zeile 33) hinter den Kulissen auf den Dropbox-Server zu, holt einen Request-Token mit Secret ein und liefert eine Login-URL zurück. Diese präsentiert die Webapplikation dann dem User, der sich auf einen Mausklick hin so bei Dropbox anmelden kann (Abbildung 5).

Abbildung 5: Nach dem Einloggen geht die Kontrolle von Dropbox.com wieder an den Mojolicious-Server über, der die Token-Daten in einer YAML-Datei speichert.
Die Mojolicious-Applikation speichert die eingeholten Werte für Request-Token und -Secret in den globalen Variablen »$REQUEST_TOKEN« und »$REQUEST_SECRET« , auf die sie später zugreift, wenn Dropbox.com den Browser nach erfolgreichem Login wieder zurück zum Gerüstserver unter der in Zeile 32 gesetzten Callback-URL sendet.
Erlangt die Mojolicious-Applikation anschließend wieder die Kontrolle, braucht sie sich nur den beigelegten Access-Token aus der Parameterliste des Callback zu schnappen und hat dann zusammen mit dem vorher gespeicherten Request-Token-Secret die Schlüssel in der Hand, um anstelle des Users nach Belieben in dessen Dropbox-Account herumzufuhrwerken. Das Skript speichert die beiden Schlüssel aber nur in der YAML-Datei »~.dropbox.yml« (Abbildung 6), damit später aufgerufene Applikationen sie sich ohne große Klimmzüge dort abholen können.
Drohnenbetrieb
Zum einfachen Zugriff auf die User-Kenndaten mit transparenter Dropbox-Bedienung definiert Listing 2 eine von Net::Dropbox::API abgeleitete Klasse »MyDropbox« , deren Konstruktor das Dropbox-Modul gleich mit den in der YAML-Datei abgelegten Schlüsseln ausstattet, sodass Applikationen, die es verwenden, sich nur noch um den eigentlichen Betrieb und nicht mehr um Authentisierungsfragen kümmern müssen. Außerdem ist so der Betrieb im Drohnenmodus möglich, bei dem kein User mehr davor sitzt oder ein Webbrowser im Spiel ist.
Listing 2
MyDropbox.pm
01 package MyDropbox;
02 use strict;
03 use base 'Net::Dropbox::API';
04 use YAML qw(LoadFile);
05
06 my $dev_key = "iyaiu823ajksgwf";
07 my $dev_secret = "zlkj32lkj2kl3dp";
08 my($home) = glob '~';
09 my $CFG_FILE = "$home/.dropbox.yml";
10
11 ###########################################
12 sub new {
13 ###########################################
14 my($class) = @_;
15
16 my $box = Net::Dropbox::API->new({
17 key => $dev_key,
18 secret => $dev_secret,
19 });
20
21 my $cfg = LoadFile( $CFG_FILE );
22 $box->access_token(
23 $cfg->{access_token} );
24 $box->access_secret(
25 $cfg->{access_secret} );
26
27 bless $box, $class;
28 }
29
30 1;
Eine einfache Beispielapplikation zeigt Listing 3, die lediglich den Kontext auf »dropbox« setzt (Produktionsbetrieb im Gegensatz zur Testumgebung »sandbox« ) und dann mit »list()« die im Folder »Photos« liegenden Dateien auflistet.
Listing 3
dropbox-dump
01 #!/usr/local/bin/perl -w 02 use strict; 03 use MyDropbox; 04 use Data::Dumper; 05 06 my $box = MyDropbox->new(); 07 $box->context( "dropbox" ); 08 09 my $href = $box->list( 10 "/Photos"); 11 12 $Data::Dumper::Indent = 1; 13 print Dumper( $href );
Zurück kommt eine verschachtelte Struktur nach Abbildung 7, aus der der Programmierer den Inhalt der Dropbox extrahieren kann, aufgeschlüsselt nach Dateinamen, -größe, dem Datum der letzten Modifizierung und einigem mehr. Weitere Methoden erlauben das Hoch- und Herunterladen sowie das Löschen von Dateien. Auch ein Test, der feststellt, ob sich seit der letzten Anfrage in einem bestimmten Teil der Dateihierarchie etwas verändert hat, wird angeboten. Dem Einfallsreichtum der Entwickler sind keine Grenzen gesetzt, solange sie das gegenwärtig eingestellte Tageslimit von 5000 Zugriffen nicht überschreiten.
Geister-Updater
Als praktische Anwendung des API habe ich mir neulich einen Geister-Updater implementiert. Die Klone der Git-Repositories, an denen ich arbeite, sind dank des hier im vergangenen Jahr vorgestellen Gitmeta-Tools [4] immer auf dem aktuellen Stand. Leider kommt es aber vor, dass ich vergessen habe, auf meinem Heimrechner »git push« auszuführen, sodass dort also lokal Änderungen verzeichnet sind, die noch nicht auf dem Git-Server liegen und somit nicht so einfach auf den Laptop im Hotelzimmer zu bekommen sind, da sich der Heimrechner hinter einer Firewall verbirgt.
Doch ein selbst geschriebener Dropbox-Daemon auf dem Heimrechner löst das Problem. Das Skript in Listing 4 fährt mit »dropbox-gitgetter start« im Hintergrund hoch und überwacht die Datei »gitgetter/requests.txt« im Dropbox-Folder periodisch alle 60 Sekunden auf Änderungen. Benötigt der Dropbox-Nutzer irgendwo aus dem Internet eine Datei des Heimrechners (Abbildung 8), fügt er deren Pfad in die Requestdatei ein und lädt die modifizierte Version der Requestdatei in die Dropbox (Abbildung 9).
Listing 4
dropbox-gitgetter
01 #!/usr/local/bin/perl -w
02 use strict;
03 use MyDropbox;
04 use App::Daemon qw( daemonize );
05 use Log::Log4perl qw(:easy);
06 use HTTP::Status qw(:constants);
07 use File::Temp qw(tempfile);
08 use Cwd qw(realpath);
09 use Sysadm::Install qw(:all);
10 use File::Basename;
11
12 Log::Log4perl->easy_init($DEBUG);
13
14 daemonize();
15
16 my $mod_hash = undef;
17 my $poll_interval = 60;
18
19 my $box = MyDropbox->new();
20 $box->context( "dropbox" );
21
22 my($home) = glob "~";
23 my $gitdir = realpath "$home/git";
24
25 while( 1 ) {
26 my @hash_args = ();
27 @hash_args = ( hash => $mod_hash ) if
28 defined $mod_hash;
29
30 my $href = $box->list( { @hash_args },
31 "/gitgetter" );
32
33 if( $href->{http_response_code} eq
34 HTTP_NOT_MODIFIED ) {
35 DEBUG "Not modified";
36 } else {
37 $mod_hash = $href->{hash};
38 request_handler( $box );
39 }
40
41 DEBUG "Sleeping ${poll_interval}s";
42 sleep $poll_interval;
43 }
44
45 ###########################################
46 sub request_handler {
47 ###########################################
48 my($box) = @_;
49
50 my $content =
51 $box->getfile("gitgetter/requests.txt");
52
53 my $pushed = 0;
54
55 for my $line ( split /\n/, $content ) {
56 $line =~ s/#.*//;
57 next if $line =~ /^\s*$/;
58 DEBUG "Found request: '$line'";
59
60 my $file = realpath( "$gitdir/$line" );
61 if( $file !~ /^$gitdir/ ) {
62 ERROR "Path $file denied.";
63 next;
64 }
65
66 DEBUG "Delivering $file";
67
68 if( !-f $file ) {
69 ERROR "$file doesn't exist";
70 next;
71 }
72
73 my $href = $box->putfile( $file,
74 "gitgetter" );
75 $pushed++;
76 }
77
78 if( $pushed ) {
79 my($fh, $tmpfile) = tempfile(
80 UNLINK => 1 );
81 blurt "# pending requests\n", $tmpfile;
82 my $href = $box->putfile( $tmpfile,
83 "gitgetter", "requests.txt" );
84 }
85 }

Abbildung 9: Kurze Zeit später hat der Daemon tatsächlich die zuvor gewünschte Datei eingeschmuggelt.
Der Daemon auf dem Heimrechner bekommt die Änderung mit. Steht in der Requestdatei eine neue Zeile mit einem Dateipfad, prüft der Daemon, ob die Anfrage sich auch wirklich auf eine Datei im Verzeichnis der Git-Repositories bezieht. Falls ja, holt er die gewünschte Datei aus dem lokalen Dateisystem des Heimrechners, pumpt sie mittels Dropbox-API in die Dropbox, löscht dann die Anfrage aus »requests.txt« und spielt die modifizierte Version der Requestdatei wieder in die Dropbox. Da Dropbox sich weigert leere Dateien hochzuladen, lässt der Daemon auch bei null verbliebenen Anfragen einen Kommentar stehen.
Im Hintergrund
Listing 4 läuft nach dem Kommando »start« im Hintergrund und unterstützt dank App::Daemon vom CPAN auch »stop« und »status« , um den Daemon herunterzufahren oder seinen Status zu ermitteln. Er loggt seine Aktivitäten mittels Log4perl in der Datei »/tmp/dropbox-gitgetter.log« . Mit »-X« startet das Skript im Vordergrund, um ein Problem einzukreisen, falls mal etwas nicht erwartungsgemäß funktioniert.
In der Endlosschleife ab Zeile 25 ruft Listing 4 alle 60 Sekunden die »list()« -Methode des »MyDropbox« -Objekts auf und findet so heraus, ob sich unter dem Dropbox-Verzeichnis »/gitgetter« etwas geändert hat. Statt aber jedes Mal die ganze Hierarchie über die Leitung zurückzupusten, nutzt das Skript die effiziente Hash-Methode.
Das Dropbox-API gibt nämlich bei »list()« -Anfragen neben dem gewünschten Resultat auch noch einen 32-Byte-Hexstring zurück, der den Status der Dateienhierarchie widerspiegelt. Legt der Bandbreite knausernde API-Programmierer den Hash beim nächsten Aufruf wieder bei, gibt der Dropbox-Server bei unverändertem Inhalt statt Daten nur den HTTP-Code 403 (»not modified« ) zurück und das Skript kann sich bis zur nächsten Runde schlafen legen.
Vorsicht vor Bösewichten
Andernfalls springt die Funktion »request_handler()« ab Zeile 46 an, die mit der Methode »getfile()« in Zeile 51 den Inhalt der Datei »gitgetter/requests.txt« über das API vom Dropbox-Server einholt. Eine For-Schleife iteriert dann über alle ihre Zeilen, während sie Kommentarzeilen entfernt und leere Zeilen ignoriert. Die Funktion »realpath()« aus dem Fundus des Moduls Cwd stellt bei gefundenen Zeilen zur Datei-Anfrage sicher, dass sich im Pfad keine Schummelzeichen befinden, die den Daemon dazu überlisten könnten, in beliebige Dateipfade zu wandern und dortige Dateien auszuliefern. Hierzu hängt das Skript den verlangten Pfad an das vorgegebene Git-Verzeichnis an und prüft anschließend, ob »realpath« auf das Ergebnis auch tatsächlich selbst wieder innerhalb eines Git-Verzeichnisses liegt.
Das ist deshalb wichtig, damit kein Bösewicht, der das Dropbox-Passwort des Users knackt oder gar den Dropbox-Server in seine Gewalt bringt, nach und nach das gesamte lokale Dateisystem ausspähen kann. Stattdessen bekäme er nur den explizit zugelassenen Pfad zu den Git-Repositories zu sehen, deren Serverversionen sowieso im Licht der Öffentlichkeit stehen.
Genehmigt der Daemon die Herausgabe der Datei, stellt sie »putfile()« in Zeile 73 ins Verzeichnis »/gitgetter« der Dropbox. Falls das Skript Requests abgearbeitet hat, schreibt Zeile 82 eine bis auf eine Kommentarzeile leere »requests.txt« -Datei zurück in die Dropbox, damit der Daemon nicht beim nächsten Mal wieder von vorne anfängt.
Installation
Den Rattenschwanz an benötigten Modulen findet man entweder in einschlägigen Paketen der verwendeten Distribution (etwa »libapp-daemon-perl« , »liblog -log4perl« und so weiter) oder alternativ über eine CPAN-Shell.
Das CPAN-Modul Net::Dropbox::API unterstützte zum Zeitpunkt der Erstellung dieses Beitrags die Hash-gestützte Dropbox-Abfrage nach modifizierten Dateien noch nicht, also habe ich das Projekt auf Github kurzerhand geforkt, die benötigte Funktion eingefügt und den Autor des Moduls um Aufnahme des Patch in die Mainline gebeten. Falls er dies bis zum Erscheinen des Artikels noch nicht bewerkstelligt haben sollte, können interessierte Leser den Tarball des modifizierten Moduls auf Github [5] herunterladen.
Um das Dropbox-API zu verwenden, benötigen Entwickler einen Developer-Key, den es unter [3] gegen Hinterlegung einer E-Mail-Adresse ohne Umstände gibt. In den Listings sind dann die Variablen »$dev_key« und »$dev_secret« entsprechend anzupassen – und los geht’s.
Mit Vorsicht genießen
Der Vorteil der Dropbox liegt klar in der einfachen Handhabung. Selbst Computerneulinge können dort Daten ablegen und anderswo wieder abholen. Der Beitrag [6] beschreibt zum Beispiel ein Verfahren zur Kommunikation zwischen Entwicklern und Webdesignern, das Änderungen in einem Git-Repository über die Dropbox auch einer Klientel zugänglich macht, die mit der Bedienung eines Git-Repositories überfordert wäre.
Übrigens versichert Dropbox.com zwar, die auf seinen Servern gespeicherten Daten seien verschlüsselt und daher selbst für Dropbox-Mitarbeiter nicht einsehbar, doch laut [7] stimmt das nicht. Aber wie dem auch sei, es gilt auch in diesem Fall: Sensitive Daten sind auf der lokalen Festplatte immer noch am sichersten aufgehoben, an ihren wolkigen Sicherheitskonzepten muss die Cloud wohl noch ein wenig feilen.
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/07/Perl
- Oauth: http://en.wikipedia.org/wiki/Oauth
- Dropbox for Developers: https://www.dropbox.com/developers/quickstart
- Michael Schilli, “Überall Projekte”: Linux Magazin 08/2010, https://www.linux-magazin.de/Heft-Abo/Ausgaben/2010/08/Ueberall-Projekte
- Net::Dropbox 1.4_01, inklusive der im Artikel verwendeten Hashfunktion auf Github: http://github.com/mschilli/Net–Dropbox/tarball/1.4_01
- Ken Mayer, “Dropbox + git = Designer Luv”: http://pivotallabs.com/users/ken/blog/articles/1637-dropbox-git-designer-luv
- “Dropbox Lied to Users About Data Security, Complaint to FTC Alleges”: http://www.wired.com/threatlevel/2011/05/dropbox-ftc










