Bitbucket – Ein Schnelleinstieg mit Mercurial unter Linux

25 05 2013

Versucht man sich an etwas größeren Programmier-Projekten, bietet es sich irgendwann an, dieses mit einer Versionsverwaltung auszustatten. Denn überschreibt man sich beispielsweise einmal  seine Dateien mit einer alten Version oder findet man partout einen Fehler nicht, kann es äußerst sinnvoll sein, die Änderungen der letzten Tage nachvollziehen zu können. Freie Software findet man unter anderem mit Git oder Mercurial, aber wohin dann mit den Dateien? Man könnte diese auf eigenem Webspace einrichten, schneller und mit weiteren Funktionen geht das mit dem Service von Bitbucket. Hier kann man sich kostenlos eigene Repositories einrichten, auf die bis zu fünf Entwickler gleichzeitig zugreifen dürfen. Wollen mehr Entwickler an dem Projekt arbeiten, lässt sich diese Anzahl gegen Gebühr in verschiedenen Abstufungen erhöhen.

Wir möchten uns doch zunächst nur ein kleines Repository für uns selbst einrichten. Dazu legen wir zuallererst einen Bitbucket Account an. Die Webseite steht in deutscher Sprache zur Verfügung, diese können wir in unserem Benutzerkonto einstellen. Hier bietet es sich auch gleich an, einen SSH Schlüssel zur Kommunikation mit der Seite ohne Passwort zu hinterlegen. Haben wir das noch nicht getan, erzeugen wir via ssh-keygen ein Schlüsselpaar, dessen Public-Key Inhalt wir in Bitbucket unter der Option „SSH Schlüssel“ -> „Schlüssel hinzufügen“ einfügen.

$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/aaaaa/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/aaaaa/.ssh/id_rsa.
Your public key has been saved in /home/aaaaa/.ssh/id_rsa.pub.

$ ls -l ~/.ssh
insgesamt 36
-rw------- 1 aaaaa aaaaa 1679 Mai 14 22:55 id_rsa
-rw-r--r-- 1 aaaaa aaaaa  398 Mai 14 22:55 id_rsa.pub

$ cat ~/.ssh/id_rsa.pub

Jetzt erzeugen wir unser Repository, in dem wir unseren Code halten möchten. Hierzu navigieren wir über „Repositorys“ -> „Repository erstellen“ und geben einen Projekt-Namen an. Legen wir fest, dass es sich um ein privates Repository handelt, kann niemand außer den definierten Entwicklern den Code einsehen, ansonsten ist dieser öffentlich verfügbar. Als Repository-Typ nutze ich Mercury, aber das muss jeder nach den eigenen Vorlieben entscheiden. Git bietet mehr Optionen, Mercury lässt sich dafür einfacher handhaben. Hinter „Ticket-Verfolgung“ versteckt sich ein Bugtracker, der auch bei privaten Code-Projekten für die Öffentlichkeit frei geschaltet werden. Selbiges gilt für das Wiki, in dem man eine Dokumentation erstellen kann. Haben wir die Programmiersprache gewählt, können wir das „Repository erstellen“.

bitbucket05

Natürlich benötigen wir noch die entsprechende Software zur Versionskontrolle. Mercurial installieren wir uns in Ubuntu über die Paketverwaltung.

$ sudo apt-get install mercurial

Damit könnten wir auf Befehlszeile direkt loslegen. Wünschen wir uns eine komfortable GUI, können wir zusätzlich auch noch EasyMercurial installieren, das zwar kaum Optionen mitbringt, dafür aber absolut simpel zu bedienen ist. Mehr Funktionen bietet TortoiseHg, Eine Einführung zu diesem Programm findet man bei Draketo, weshalb ich mir das hier spare. Auch für Eclipse finden sich mit EGit und MercurialEclipse zwei gute Plugins, welche bei heise kurz vorgestellt werden.

Beim ersten Start von EasyMercurial kümmern wir uns zunächst um die Einstellungen im Menü „File“ -> „Settings“. Im Tab „User details“ geben wir unseren Namen und die E-Mail Adresse ein. Unter „Remote“ -> „Set Push and Pull Location…“ tragen wir das gerade erstellte Bitbucket Repository ein. Die genaue Bezeichnung finden wir, wenn wir den Link „Ich habe ein bestehendes Projekt zum hoch pushen“ aufrufen.

bitbucket01 bitbucket07  bitbucket08

Mit einem Klick auf „Open“ laden wir nun unser Projekt-Verzeichnis als lokales Repository.

bitbucket02 bitbucket03

Im sich öffnenden erscheinenden Fenster wählen wir alle Dateien, die für unser Projekt notwendig sind. Diese selektieren wir und klicken auf den „Add“ Button, Cache oder sonstige temporäre Dateien bleiben außen vor.

bitbucket04  bitbucket06

Um diese Dateien zu übernehmen, bedarf es noch eines Commits. Hier tragen wir normalerweise einen Kommentar ein, was sich seit dem letzten Stand geändert hat. In unserem Fall ist dies die erste Version, weshalb wir dies auch so angeben. Ist das getan, senden wir die Dateien per „Push“ an das Remote Repository auf unserem Bitbucket Account. Je nach Anzahl und Größe der Dateien kann das einen Moment dauern. Dass sich etwas tut erkennen wir an dem psychedelischen Balken, der sich am unteren Bildrand hektisch hin- und herbewegt.

bitbucket09 bitbucket10 bitbucket11

Sind die Dateien auf dem Bitbucket Server angekommen, können wir diese nun in unserem Account unter dem Punkt „Quellcode“ sehen.

bitbucket12

Ändern wir unsere Dateien auf der heimischen Platte, erkennt das EasyMercurial automatisch und zeigt uns die modifizierten Dateien an. Möchten wir diese synchronisieren, gehen wir wie zuvor mit einem Commit und anschließendem Push vor. Aussagekräftige Beschreibungen beim Commit helfen uns später dabei, zu erkennen welche Modifikationen wir vorgenommen haben. In der History sehen wir die verschiedenen Commit-Punkte grafisch dargestellt.

bitbucket13

Advertisements




Installation eines LAMP Webservers mit Ubuntu

22 08 2012

Normalerweise nutze ich für meinen Test-Webserver die XAMPP Sammlung, da dieses keinerlei Installation erfordert und das komplette Paket portabel ist, also beispielsweise mit einem USB Stick überall hin mitgenommen und auf einem anderen Computer weiter verwendet werden kann. Trotzdem kann es manchmal von Vorteil sein, die Komponenten direkt zu installieren.

Die Mär, dass eine Installation eines funktionierenden Webservers mit allen wichtigen Komponenten für Testzwecke schwierig sei möchte ich heute ausräumen. Natürlich taugt ein solches System noch nicht für den Produktiveinsatz, sollte also nicht öffentlich an das Netz angebunden werden. Dazu benötigt es noch einigen Feinschliff insbesondere was die Sicherheit gegenüber böswilligen Angriffen angeht.

Im Folgenden wollen wir uns die gebräuchlichste Software installieren. Die wären der Apache Webserver, als Datenbank MySQL sowie die Skriptsprache PHP. Nimmt man dann noch unser Betriebssystem hinzu, wissen wir jetzt auch, wofür LAMP steht, nämlich für Linux, Apache, MySQL, PHP.

Was das genau für Programme sind, was man damit überhaupt anstellen kann, das ist nicht Inhalt dieses Artikels. Hier soll es lediglich um die Installation in Linux gehen. Ein grundsätzliches Verständnis der Materie wird also vorausgesetzt.

Apache
Installieren des Apache Webservers:

sudo apt-get install apache2

Der Server soll nicht von außen erreichbar sein. Deshalb sagen wir Apache, dass er nur auf localhost hören soll. Hierzu bearbeiten wir die ports.conf:

sudo vi /etc/apache2/ports.conf

Listen 80
ändern in
Listen localhost:80

Jetzt können wir unseren Webserver schon testen, indem wir die Adresse http://localhost/ in unsere Browser-Adresszeile eintippen.

Wollen wir Subdomains erstellen, kopieren wir uns das Default-Template aus /etc/apache2/sites-available in eine neue Datei mit dem Namen unseres virtuellen Hosts und editieren diese. Meine Subdomain heißt „ramtatta“, außerdem habe ich den Default-Pfad von /var/www/ noch abgeändert in mein Home-Verzeichnis.

sudo cp /etc/apache2/sites-available/default /etc/apache2/sites-available/ramtatta
sudo vi /etc/apache2/sites-available/ramtatta

<VirtualHost *:80>
   ServerName ramtatta.localhost
   ServerAdmin webmaster@localhost
   DocumentRoot /home/aaaaa/htdocs/RamTatTa
   <Directory /home/aaaaa/htdocs/RamTatTa/>
      Options Indexes FollowSymLinks MultiViews
      AllowOverride All
      # Die folgenden beiden Einträge waren unter Apache 2.2 gültig.
      # Order allow,deny
      # allow from all
      # Für Apache 2.4 müssen diese durch den nachstehenden Eintrag ersetzt werden:
      Require all granted
   </Directory>
   ErrorLog ${APACHE_LOG_DIR}/error.log
   LogLevel warn
   CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Damit die Umleitung funktioniert, müssen wir noch unsere hosts erweitern.
sudo vi /etc/hosts

Hinzufügen dieser Zeile:
127.0.0.1 ramtatta.localhost

Jetzt muss der virtuelle Host noch aktiviert und der Apache durchgestartet werden.
sudo a2ensite
sudo /etc/init.d/apache2 restart

Aufrufen können wir die Subdomain folgendermaßen: http://ramtatta.localhost/

Möchten wir unsere Links mittels mod_rewrite verschönern, so müssen wir dieses ggf. auch erst noch aktivieren.
sudo a2enmod rewrite
sudo /etc/init.d/apache2 restart

PHP
Installieren von PHP:

sudo apt-get install php libapache2-mod-php

Unsere HTML Seiten legen wir per Default in /var/www ab. PHP testen wir, indem wir hier eine Datei index.php mit folgendem Inhalt erzeugen.

vi /var/www/index.php
   <?php
   phpinfo();
   ?>

Laden wir jetzt wieder http://localhost/ im Browser, wird eine Seite mit vielen Informationen zu unserer Serverumgebung dargestellt.

MySQL
Installieren von MySQL. Den de facto Standard zur Administration von MySQL Datenbanken im Web stellt PHPMyAdmin dar. Um uns gleich daran zu gewöhnen, installieren wir das gleich mit.

sudo apt-get install mysql-server php5-mysql phpmyadmin

Ich selbst habe mich selbst an die Software Navicat gewöhnt. Auch in der kostenlosen Version bietet dieses Programm sehr viele schöne Funktionen und ist in seiner Eigenschaft als echtes Programm jederzeit flotter als die Weboberfläche von PHPMyAdmin. Einziges Manko ist, dass es sich eigentlich um Windows-Binaries handelt, das lediglich gebündelt mit der Wine Laufzeitumgebung als Linux-Version angeboten werden.

Auf die Datenbank können wir jetzt also auch zugreifen. Hierzu tippen wir in der Shell

mysql -u root

bzw. laden im Browser die Adresse http://localhost/phpmyadmin.

Eclipse
Einen sehr mächtigen Editor für die Entwicklung unserer PHP Dateien finden wir in Eclipse. Dieser ist grundsätzlich für die Java-Entwicklung gedacht, mit den PHP Development Tools (PDT) können wir diesen aber auch sehr gut für PHP verwenden.

Installation des Grundpaketes für Eclipse:

sudo apt-get install eclipse

Das Programm starten wir und klicken uns durch zur automatischen Installation der PDT:
Help -> Install new software… -> Work with: Indigo Update Site wählen -> Programming Languages -> PHP Development Tools (PDT) -> Installieren

Xdebug
Ein großer Nachteil von PHP ist, dass sich dieses normalerweise nicht wirklich debuggen lässt. Abhilfe schafft die Xdebug Erweiterung. Wir installieren zunächst das Paket.

sudo apt-get install php5-xdebug

Jetzt müssen wir das Binary und die Ini-Datei finden. Dazu aktualisieren wir unsere Datenbank für locate und lassen uns dann den Pfad für die xdebug.so und xdebug.ini ausgeben.

sudo updatedb

locate xdebug.so
/usr/lib/php5/20090626/xdebug.so

locate xdebug.ini
/etc/php5/cli/conf.d/xdebug.ini

Die Ini Datei editieren wir. Normalerweise sollte hier der Pfad für die Binary schon eingetragen sein, wir kontrollieren dies aber zur Sicherheit. Dann tragen wir noch ein paar Variablen ein, mit denen wir Xdebug aktivieren und starten den Apache neu.

sudo vi /etc/php5/cli/conf.d/xdebug.ini
   zend_extension=/usr/lib/php5/20090626/xdebug.so
   xdebug.remote_enable=1
   xdebug.remote_handler=dbgp
   xdebug.remote_mode=req
   xdebug.remote_host=127.0.0.1
   xdebug.remote_port=9000
sudo /etc/init.d/apache2 restart




Sichere Dateiuploads mit PHP

4 06 2012

Stellen wir auf unserer Homepage den Besucher über einen Dateiupload die Möglichkeit zur Verfügung, eigene Dateien auf unseren Server zu übertragen, ist eine gewisse Vorsicht anzuraten. Immerhin möchten wir keinen ausführbaren Code auf unseren Server eingeschleust bekommen.

Um die übertragenen Dateien zu prüfen, könnten wir beispielsweise auf die Idee kommen, die $_FILES Variable auszulesen, die nach einem Upload vom Server gefüllt wird. Schauen wir uns diese doch anhand eines kleinen Beispiels einmal an.

<!-- Formular, um eine Datei hochzuladen -->
<form enctype="multipart/form-data" action="index.php" method="post">
Datei hochladen: <input name="datei" type="file" /><br />
<input type="submit" />
</form>

<?php
if (!empty($_FILES)){
	var_dump($_FILES);
}

Ausgabe:

Array
(
    [datei] => Array
        (
            [name] => avatar.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpLgZOUQ
            [error] => 0
            [size] => 8801
        )

)

Im vollständigen Beispiel sähe das dann in etwa so aus:

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('image/gif', 'image/jpeg', 'image/png');	// Erlaubte Dateiformate
	$dateiname = $upload_pfad.basename($_FILES['datei']['name']);	// Dateiname inkl. Pfad

	echo '<p>Erkanntes Dateiformat: '.$_FILES['datei']['type'].'</p>';	// Ausgabe

	if (in_array($_FILES['datei']['type'], $erlaubte_formate)){	// Wenn es sich um eine datei.jpg handelt, gehen wir davon aus, dass es sich um eine Grafik handelt
		if(!move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/'.$_FILES['datei']['name'])) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Ausgabe:

Erkanntes Dateiformat: image/jpeg

<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Nur leider kommen diese Werte vom Browser und lassen sich von daher relativ einfach manipulieren. Wir können uns darauf also nicht verlassen, da wir böswilligen Angreifern somit Tür und Tor öffnen. Die Unzuverlässigkeit erkennen wir deutlich daran, wenn wir unser Jpeg-Bild umbenennen in avatar.exe und den Upload damit testen.

Ausgabe:

Erkanntes Dateiformat: application/x-ms-dos-executable

An der Datei hat sich nichts geändert außer der Erweiterung. Es handelt sich also weiterhin um die gleiche Grafik, nur mit einem fehlerhaften Namenszusatz. Gefährlicher wird es, wenn beispielsweise eine EXE-Datei umbenannt wird, hier könnte einfach eine Bilddatei vorgetäuscht werden.

Somit erkennen wir schon, dass wir uns auch nicht einfach auf die Dateinamenserweiterung verlassen dürfen. Heißt die hochgeladene Datei schadcode.jpg, können wir keineswegs davon ausgehen, dass drin ist, was drauf steht.

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('gif', 'jpg', 'png');	// Erlaubte Dateiformate
	$dateiname = $upload_pfad.basename($_FILES['datei']['name']);	// Dateiname inkl. Pfad

	$dateiinfo = pathinfo($_FILES['datei']['name']);
	$dateiformat = $dateiinfo['extension'];	// Auslesen der Dateierweiterung
	echo '<p>Erkanntes Dateiformat: '.$dateiformat.'</p>';	// Ausgabe

	if (in_array($dateiformat, $erlaubte_formate)){	// Wenn es sich um eine datei.jpg handelt, gehen wir davon aus, dass es sich um eine Grafik handelt
		if(!move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/'.$_FILES['datei']['name'])) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Ausgabe:

Erkanntes Dateiformat: jpg

Auch dies ist keine geeignete Methode, denn die Erweiterung ist schnell verändert. Im Windows lässt sich zwar eine schadcode.jpg nicht starten, weil hier nur bestimmte Dateiformate ausgeführt werden, doch die meisten Webserver laufen unter einem Linux Derivat. Hier ist die Erweiterung vollkommen egal, es kommt nur darauf an, dass die Datei das Executable-Bit gesetzt hat. Im Beispiel sei verdeutlicht, dass auch eine .jpg Datei problemlos ausgeführt werden kann.

$ echo "echo Ich bin ausführbar" >schadcode.jpg
$ chmod u+x schadcode.jpg 
$ ./schadcode.jpg 
Ich bin ausführbar

Von daher sollten wir fremde Dateien niemals auf unserem System ausführen lassen. Idealerweise speichert man diese auch nicht innerhalb der vom Webserver lesbaren Dateistruktur (normalerweise htdocs), so dass diese gar nicht direkt vom Browser aus angesprochen werden können. Über unsere Skripte, die sich ja direkt auf dem Server befinden, sollten je nach Hoster auch andere Verzeichnisse ansprechbar sein. Auf ein Beispiel hierzu verzichte ich, da das etwas zu weit führen würde.

Um das Dateiformat einigermaßen sicher zu bestimmen, müssen wir den MIME Header der Datei lesen. Ab PHP >= 5.3 können wir dazu die finfo_file Funktion mit FILEINFO_MIME_TYPE nutzen. Für ältere PHP Versionen steht das Pendant mime_content_type zur Verfügung.

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('image/gif', 'image/jpeg', 'image/png');	// Erlaubte Dateiformate
	$dateiname = $upload_pfad.basename($_FILES['datei']['name']);	// Dateiname inkl. Pfad

	$finfo = finfo_open(FILEINFO_MIME_TYPE);	// Gib den MIME Typ zurueck
	if ($finfo !== FALSE && !empty($finfo)){
	   	$dateiformat = finfo_file($finfo, $_FILES['datei']['tmp_name']);
		finfo_close($finfo);
	} else die('Fileinfo Datenbank konnte nicht geoeffnet werden');

	echo '<p>Erkanntes Dateiformat: '.$dateiformat.'</p>';	// Ausgabe

	if (in_array($dateiformat, $erlaubte_formate)){	// Dieser Dateityp entspricht unseren Vorgaben
		if(!move_uploaded_file($_FILES['datei']['tmp_name'], 'uploads/'.$_FILES['datei']['name'])) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Ausgabe:

Erkanntes Dateiformat: image/jpeg
<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Nun sollte man sich auch nicht auf den Mime-Typ alleine verlassen. So würde nämlich auch eine schadcode.exe Datei akzeptiert werden, wenn nur der Mime-Typ passt. Laden wir doch einfach unsere avatar.exe hoch.

Erkanntes Dateiformat: image/jpeg
<img src="uploads/avatar.exe" title="Hochgeladenes Bild" />

Von daher sollte man die Erweiterung anhand des erkannten Typs stets selbst setzen, um absichtliche wie auch unabsichtliche Nutzerfehler zu vermeiden.

<?php
if (!empty($_FILES)){
	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array(
		'image/gif' => 'gif',
		'image/jpeg' => 'jpg',
		'image/png' => 'png');	// Erlaubte Dateiformate. Im multidimensionalen Array sind nun auch die zugehoerigen Dateierweiterungen angegeben

	$dateiname = $upload_pfad.pathinfo($_FILES['datei']['name'], PATHINFO_FILENAME);	// Mit pathinfo holen wir nur den Dateinamen ohne Erweiterung

	$finfo = finfo_open(FILEINFO_MIME_TYPE);	// Gib den MIME Typ zurueck
	if ($finfo !== FALSE && !empty($finfo) > 0){
	   	$dateiformat = finfo_file($finfo, $_FILES['datei']['tmp_name']);
		finfo_close($finfo);
	} else die('Konnte das Dateiformat nicht erkennen');

	echo '<p>Erkanntes Dateiformat: '.$dateiformat.'<br />';	// Ausgabe

	if (isset($erlaubte_formate[$dateiformat])){	// Existiert ein Eintrag mit dem Key?
		$dateiname .= '.'.$erlaubte_formate[$dateiformat];	// Haenge die Erweiterung an den Dateinamen an

		echo 'Ursprünglicher Dateiname: '.$_FILES['datei']['name'].'<br />';
		echo 'Aktueller Name: '.basename($dateiname).'</p>';

		if(!move_uploaded_file($_FILES['datei']['tmp_name'], $dateiname)) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Die Ausgabe, wenn wir wieder unsere avatar.exe hochladen:

Erkanntes Dateiformat: image/jpeg
Ursprünglicher Dateiname: avatar.exe
Aktueller Name: avatar.jpg

<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Abschließend sei noch auf getID3() hingewiesen. Dieses umfangreiche Skript liest die verschiedensten Multimedia-Formate ein und liefert neben den Header Informationen noch viele weitere nützliche Informationen. In unserem Beispiel sähe der Einsatz dann so aus:

<?php
if (!empty($_FILES)){
	require_once('getid3/getid3.php');	// getID3() einbinden
	$getID3 = new getID3;

	$upload_pfad = 'uploads/';	// Wo sollen die hochgeladenen Dateien abgelegt werden?
	$erlaubte_formate = array('image/gif', 'image/jpeg', 'image/png');	// Erlaubte Dateiformate

	$dateiname = $upload_pfad.pathinfo($_FILES['datei']['name'], PATHINFO_FILENAME);	// Mit pathinfo holen wir nur den Dateinamen ohne Erweiterung

	$dateiinfo = $getID3->analyze($_FILES['datei']['tmp_name']);
	var_dump($dateiinfo);

	echo '<p>Erkanntes Dateiformat: '.$dateiinfo['mime_type'].'<br />';	// Ausgabe

	if (in_array($dateiinfo['mime_type'], $erlaubte_formate)){	// Dieser Dateityp entspricht unseren Vorgaben
		$dateiname .= '.'.$dateiinfo['fileformat'];	// Haenge die Erweiterung an den Dateinamen an

		echo 'Ursprünglicher Dateiname: '.$_FILES['datei']['name'].'<br />';
		echo 'Aktueller Name: '.basename($dateiname).'</p>';

		if(!move_uploaded_file($_FILES['datei']['tmp_name'], $dateiname)) die('Konnte die Datei nicht hochladen');	// Datei in ihr Verzeichnis schieben
		echo '<img src="'.$dateiname.'" title="Hochgeladenes Bild" />';	// Ausgabe der Datei im Browser
	}
}

Zur besseren Verdeutlichung erfolgt zunächst die Ausgabe des von getID3() gelieferten Arrays mit den Dateiinformationen. Danach folgt unsere normale Ausgabe. Da auch die Erweiterung geliefert wird, müssen wir diese nicht mehr selbst definieren:

array(11) {
  ["GETID3_VERSION"]=>
  string(14) "1.9.3-20111213"
  ["filesize"]=>
  int(8801)
  ["filename"]=>
  string(9) "phpb1p7Qd"
  ["filepath"]=>
  string(4) "/tmp"
  ["filenamepath"]=>
  string(14) "/tmp/phpb1p7Qd"
  ["avdataoffset"]=>
  int(0)
  ["avdataend"]=>
  int(8801)
  ["fileformat"]=>
  string(3) "jpg"
  ["video"]=>
  array(7) {
    ["dataformat"]=>
    string(3) "jpg"
    ["lossless"]=>
    bool(false)
    ["bits_per_sample"]=>
    int(24)
    ["pixel_aspect_ratio"]=>
    float(1)
    ["resolution_x"]=>
    int(72)
    ["resolution_y"]=>
    int(100)
    ["compression_ratio"]=>
    float(0.4074537037037)
  }
  ["encoding"]=>
  string(5) "UTF-8"
  ["mime_type"]=>
  string(10) "image/jpeg"
}

Erkanntes Dateiformat: image/jpeg
Ursprünglicher Dateiname: avatar.exe
Aktueller Name: avatar.jpg

<img src="uploads/avatar.jpg" title="Hochgeladenes Bild" />

Ist das jetzt wirklich sicher? Einigermaßen, doch muss für die maximale Sicherheit immer der maximale Aufwand betrieben werden. Mit Erkennung des Mime-Headers und idealerweise der Ablage der Dateien außerhalb des Web-Roots sollte man zumindest einigermaßen auf der sicheren Seite sein. Sicherer immerhin, als sich auf die durch $_FILES oder getimagesitze gelieferten Informationen zu verlassen. Letzteres hat sich in der Hinsicht in der Vergangenheit immer wieder durch seine Angreifbarkeit disqualifiziert.





Verwendung der Klasse jquery-oembed-all

13 05 2012

Im Artikel „Medien einbinden mittels oEmbed“ hatte ich beschrieben, wie man mit PHP auf die oEmbed Daten von externen Anbietern zugreift. Nun macht das nur in eher wenigen Fällen Sinn, dies per PHP zu tun. Möchte man dynamisch die Daten beim Laden seiner Seite abgreifen, verzögert sich dies, bis der externe Dienst eine Antwort schickt. Steht dieser gerade nicht zur Verfügung oder hat Performanceprobleme, wirkt sich das konkret auf die Ladegeschwindigkeit der eigenen Seite aus. Nun könnte man die Daten auch einmalig abgreifen und in der eigenen Datenbank speichern. Was aber, wenn der Anbieter seinen Einbettungscode ändert?

Sinnvoller ist es, die eigene Seite zu laden und erst wenn diese angezeigt wird mittels JavaScript die externen Informationen im Nachhinein abzugreifen. Somit erübrigen sich die eben genannten Nachteile. Mit jquery-oembed-all existiert eine gute Klasse für jQuery, mit der sich genau dies bewerkstelligen lässt. Die Klasse ist ein Fork des in jüngster Vergangenheit vom Entwickler etwas stiefmütterlich behandelten  jquery-oembed, die im Gegensatz zum Original nicht auf den offensichtlich eingestellten Dienst oohembed angewiesen ist und, sollte oEmbed nicht zur Verfügung stehen alternative Methoden zur Einbindung nutzt.

Leider mangelt es jquery-oembed-all an einer Dokumentation, weshalb ich hier einen kurzen Überblick über die Funktionen geben möchte. Beginnen wir mit einem einfachen Beispiel. Dieses platziert das einzubindende Objekt in das Div „container“.

<html>
<head>
<!-- jQuery laden -->
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<!-- die oembed-all Klasse laden -->
<script type="text/javascript" src="jquery.oembed.js"></script>
</head>

<body>
<script type="text/javascript">
// Platziere das Objekt aus .oembed im Element mit der id "container"
 $(document).ready(function() {
    $("#container").oembed("http://vimeo.com/1084537");
});
</script>
<!-- An dieser Position wird das Video angezeigt -->
<div id="container"></div>
</body>
</html>

Ergebnis:

Nun haben wir nicht nur ein Objekt zur Einbindung, sondern möchten neben dem Video noch ein Bild von Flickr anzeigen. Das zweite Beispiel platziert für jeden Link mit der Klasse „oembed“ das entsprechende Objekt.

<html>
<head>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript" src="jquery.oembed.js"></script>
</head>

<body>
<script type="text/javascript">
$(document).ready(function() {
 // Suche in jedem Element a mit der Klasse "oembed"
 $("a.oembed").oembed();
});
</script>
<div><a class="oembed" href="http://www.flickr.com/photos/kl/2423315026/">Flickr Bild</a></div>
<div><a class="oembed" href="http://vimeo.com/1084537">Vimeo Video</a></div>
</body>
</html>

Ergebnis:

Es können verschiedene Optionen angewendet werden.
autoplay (true / false): Soll ein Video automatisch gestartet werden?
maxWidth: Die maximale Breite für das Objekt
maxHeight: Die maximale Höhe für das Objekt
embedMethod: Wie soll das Objekt eingebunden werden?
– + append: Setze das Objekt unter den Link
– + fill: Ersetze den Link durch das Objekt, erhalte aber die Klasse
– + replace: Ersetze den Link und die Klasse durch das Objekt

Zur Veranschaulichung der Optionen folgt unser nächstes Beispiel. Es ist anzumerken, dass globale Optionen gesetzt werden können, die dann für jeden Dienst gelten, aber auch pro Dienst eigene Einstellungen möglich sind.

<html>
<head>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript" src="jquery.oembed.js"></script>
</head>

<body>
<script type="text/javascript">
    $(document).ready(function() {
    $(".oembed").oembed(null, {
        // Globale Optionen
        embedMethod: "append", // Haenge an
        maxWidth: 640, // Maximale Breite: 640 Pixel
        maxHeight: 480, // Maximale Hoehe: 480 Pixel
        vimeo: { // Diese Optionen gelten ausschliesslich fuer den Anbieter Vimeo
        	autoplay: true, 
        	maxWidth: 400, 
        	maxHeight: 400
        }
    });
});
</script>
<div><a class="oembed" href="http://www.flickr.com/photos/kl/2423315026/">Flickr Bild</a></div>
<div><a class="oembed" href="http://vimeo.com/1084537">Vimeo Video</a></div>
</body>
</html>

Das Video wurde auf die angegebene Maximalgröße verkleinert. Da die Einbettungsmethode „append“ verwendet wurde, werden die Objekte an die Links angehängt.

Mit der Methode „fill“ wird zwar der Link ersetzt, die Klasse .oembed bleibt allerdings erhalten. Die Klassen sind im Screenshot zur Verdeutlichung angezeigt.

„replace“ letztendlich ersetzt den kompletten Link inklusive seiner Klasse durch das einzubettende Objekt. Dies erkennt man am Besten im Vergleich der Klassen-Namen.





PHP: Daten innerhalb von HTML Tags extrahieren

5 05 2012

Um an die Links in einem String zu gelangen, sieht man immer wieder abenteuerliche Regex http://de3.php.net/regex Konstrukte. Eines haben diese neben der schlechten Lesbarkeit häufig gemeinsam, nämlich dass sie nicht zuverlässig arbeiten. Statt also das Rad neu zu erfinden, sollte man hier besser auf die gegebenen Funktionen des Document Object Models zurück greifen.

Wir wollen also aus einem Text alle Werte in <a href=“http://www.example.org/„> heraus filtern. Zu beachten ist, dass damit keine unverlinkten URLs gesucht werden, sondern nur der Inhalt innerhalb des href=“…“

<?php

$text = 'Lorem ipsum dolor sit amet, consetetur http://example.org sadipscing 
elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam 
erat, sed diam voluptua. At <a 
href="http://suchen.mobile.de/auto/search.html?scopeId=C&isSearchRequest=true&sortOption.sortBy=price.consumerGrossEuro">vero</a> 
eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 
sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, 
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et 
dolore magna aliquyam erat, sed diam voluptua. <a 
href="http://www.example.com#link">www.example.net</a>At vero eos et 
accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea 
takimata sanctus est Lorem ipsum dolor sit amet.';

// Erzeuge ein neues DOM Dokument
$dom = new DOMDocument();
// Keine Fehlerpruefung erzwingen
$dom->strictErrorChecking = false;
// Den String einlesen
@$dom->loadHTML($text);
// Die Links innerhalb des <a ...> Tags suchen
$links = $dom->getElementsByTagName('a');
// Fuer alle gefundenen Links
foreach($links as $link) {
	// Wenn der a Tag ein Attribut href hat...
	if ($link->hasAttribute('href')) {
		// ...den Inhalt auslesen
		$href = $link->getAttribute('href');
		// Bildschirmausgabe
		echo $href.'<br />';
   }
}

Die Ausgabe liefert uns beide Links:
http://suchen.mobile.de/auto/search.html?scopeId=C&isSearchRequest=true&sortOption.sortBy=price.consumerGrossEuro
http://www.example.com#link

Mittels XPath kann man auch innerhalb beliebiger Tags nach Inhalten suchen. Nehmen wir an, wir möchten alle Texte innerhalb der <td>…</td> Tags von Tabellen. Das Beispiel entsprechend angepasst:

<?php

$text = 'Lorem ipsum dolor sit amet, consetetur http://example.org sadipscing 
elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam 
erat, sed diam voluptua. At <a 
href="http://suchen.mobile.de/auto/search.html?scopeId=C&isSearchRequest=true&sortOption.sortBy=price.consumerGrossEuro">vero</a> 
eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 
sea takimata sanctus est Lorem ipsum dolor sit amet. 
<table>
<thead>
<tr><th>Kopf</th></tr>
</thead>
<tbody>
<tr><td>Zeile 1</td></tr>
<tr><td>Zeile 2</td></tr>
<tr><td>Zeile 3</td></tr>
</tbody>
</table>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 
tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. 
<a href="http://www.example.com#link">www.example.net</a>At vero eos et 
accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea 
takimata sanctus est Lorem ipsum dolor sit amet.';

$dom = new DOMDocument();
$dom->strictErrorChecking = false;
@$dom->loadHTML($text);
// XPath erzeugen
$xpath = new DOMXPath($dom);
// Der Pfad zu unseren gewuenschten Inhalten: table -> tbody -> tr -> td
// Würden wir den Wert eines Attributs suche, müssten wir dieses mit @ kennzeichnen
// Beispiel: $xpath->query('//a/@href');
$inhalte = $xpath->query('//table/tbody/tr/td');
foreach($inhalte as $inhalt) {
    echo $inhalt->nodeValue.'<br />';
}

Als Ausgabe erhalten wir:

Zeile 1
Zeile 2
Zeile 3




Medien einbinden mittels oEmbed

5 05 2012

Auf einer dynamischen Webseite das Einbinden von Object-Elementen zuzulassen birgt gewisse Risiken. Möchte man ein YouTube Video auf seiner eigenen Homepage anzeigen, präsentiert einem die Video-Plattform dazu den Code in Form eines Object-Elements bzw. neuerdings als Iframe an. Hiermit stehen die Tore für einen böswilligen Angriff weit offen, denn was genau da eingebunden wird lässt sich nur schwerlich automatisiert kontrollieren. Auf vielen Social Network Plattformen wird das Video automatisch angezeigt, sobald man lediglich die URL zur entsprechenden Seite einfügt. Doch wie funktioniert das?

Auch wenn die Nutzer dies nicht tun dürfen, kommt man nicht umhin, den Einbettungs-Code des entsprechenden Anbieters in seine Seite einzubinden. Doch wie kommt man da ran? Eine recht einfache Möglichkeit hierzu bietet oEmbed, das unter anderem von YouTube, Vimeo oder Soundcloud unterstützt wird. Über die API können wir auf die vom Anbieter zur Verfügung gestellten Informationen zugreifen. Verdeutlichen wir dies anhand eines Beispiels.

Wir möchten Big Buck Bunny von Vimeo auf unserer Seite einbinden. Hierzu benötigen wir zunächst die URL, unter der dieses Video eingebunden ist. Diese lautet http://vimeo.com/1084537. Praktischerweise finden wir auf oembed.com bereits eine Beschreibung für diesen Anbieter. Die Liste ist nicht vollständig, es kann sich also durchaus lohnen, in der jeweiligen Dokumentation nach dem Stichwort oEmbed zu suchen.

<?php

$vimeo = 'http://vimeo.com/api/oembed.xml?url=';
$video = 'http://vimeo.com/1084537';

// Zusammenbauen der URL
$url = $vimeo.urlencode($video);
// Holen der Informationen vom Anbieter. Wir nutzen das XML Format,
// meist wird aber auch JSON angeboten
$daten = @simplexml_load_file($url);
// Fehlerpruefung
if (preg_match('#^HTTP/... 4..#', $http_response_header[0])) die ('Kein Zugriff auf die Daten möglich: '.substr($http_response_header[0], 9));
// Ausgabe des Videos
echo $daten->html;

Das war es auch schon. In unserem Objekt haben wir alle Informationen zu dem Video, inklusive dem HTML Tag zur Einbindung. Hier die vollständige Ausgabe von $daten:

SimpleXMLElement Object
(
    [type] => video
    [version] => 1.0
    [provider_name] => Vimeo
    [provider_url] => http://vimeo.com/
    [title] => Big Buck Bunny
    [author_name] => Blender Foundation
    [author_url] => http://vimeo.com/user508904
    [is_plus] => 0
    [html] => < iframe src="http://player.vimeo.com/video/1084537" width="1280" height="720" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>
    [width] => 1280
    [height] => 720
    [duration] => 596
    [description] => Big Buck Bunny tells the story of a giant rabbit with a heart bigger than himself. When one sunny day three rodents rudely harass him, something snaps... and the rabbit ain't no bunny anymore! In the typical cartoon tradition he prepares the nasty rodents a comical revenge.  Licensed under the Creative Commons Attribution license  http://www.bigbuckbunny.org
    [thumbnail_url] => http://b.vimeocdn.com/ts/209/636/20963649_1280.jpg
    [thumbnail_width] => 1280
    [thumbnail_height] => 720
    [video_id] => 1084537
)

Da einige Anbieter teilweise ihre Medien gegen das Einbinden auf fremden Webseiten schützen oder auch mal ein Medium gelöscht werden kann, müssen wir prüfen, ob beim Zugriff alles in Ordnung ist. Sollte dies nicht der Fall sein, erhalten wir im Response Header einen entsprechenden HTTP Status Code. Greifen wir auf ein geschütztes Medium zu, sähe der Inhalt von $http_response_header beispielsweise so aus:

Array
(
    [0] => HTTP/1.1 403 Forbidden
    [1] => Server: nginx
    [2] => Date: Sat, 05 May 2012 11:05:32 GMT
    [3] => Content-Type: text/html; charset=utf-8
    [4] => Connection: close
    [5] => Cache-Control: no-cache
    [6] => Access-Control-Allow-Methods: GET, PUT, POST, DELETE
    [7] => Access-Control-Allow-Headers: Accept, Authorization, Content-Type, Origin
    [8] => Access-Control-Allow-Origin: *
    [9] => X-Runtime: 36
    [10] => X-Cacheable: NO:Cache-Control=no-cache
    [11] => Content-Length: 1
    [12] => X-Varnish: 1789823721
    [13] => Age: 0
    [14] => Via: 1.1 varnish
    [15] => X-Cache: MISS
)

Die Nutzung ist nicht nur auf Videos beschränkt, sondern kann für weitere Multimedia-Formate wie Audio oder Bilder genutzt werden. Um beispielsweise den Player von SoundCloud einzubinden, müssen wir lediglich den Link abändern. Die Dokumentation liefert uns eine Beschreibung aller möglichen Parameter. Gekürztes Beispiel:

<?php

$soundcloud = 'http://soundcloud.com/oembed?format=xml&show_comments=false&url=';
$audio = 'http://soundcloud.com/beastieboys/sets/hot-sauce-committee-part-two';

$url = $soundcloud.urlencode($audio);
$daten = @simplexml_load_file($url);

echo $daten->html;

Die komplette Ausgabe:

SimpleXMLElement Object
(
    [version] => 1.0
    [type] => rich
    [provider-name] => SoundCloud
    [provider-url] => http://soundcloud.com
    [height] => 450
    [width] => 100%
    [title] => Hot Sauce Committee Part Two by Beastie Boys
    [description] => 2011 release, the long-awaited eighth album from the Hip Hop/Rock trio. Hot Sauce Committee Part Two was produced by Beastie Boys and mixed by Philippe Zdar. This album marks Mike "Mike D" Diamond, Adam "Ad Rock" Horovitz and Adam "MCA" Yauch's first full length effort since 2007's Grammy-winning all-instrumental The Mix-Up.
    [html] => <![CDATA[http://w.soundcloud.com/player/?url=http%3A%2F%2Fapi.soundcloud.com%2Fplaylists%2F737966&show_artwork=true]]>
)





Berechnung der zu fahrenden Distanz zwischen zwei Orten

7 04 2012

Bei der Punkkonzerte Datenbank setze ich eine Umkreissuche ein. Hierzu verwende ich die Daten der OpenGeoDB. So ist es möglich, nur nach Konzerten zu suchen, die in einem Umkreis von beispielsweise 50 km von meinem Heimatort stattfinden. Schön und gut, nur erfolgt diese Berechnung immer nach der Luftlinie, also dem kürzesten Weg von A nach B. Hier zeigt sich jedoch aufgrund der Straßenführung teilweise eine hohe Diskrepanz zur tatsächlich zu fahrenden Strecke.

Glücklicherweise stellen uns aktuelle Online-Routenplaner ihre Daten zur Berechnung des Straßen-Weges mittels einer API zur Verfügung. Freie Daten habe ich hierzu leider noch keine finden können. Die OpenStreetMap stellt ihre Daten zwar kostenfrei zum Download zur Verfügung, doch für ein paar Abfragen lohnt es erstmal nicht, sich gigabyteweise Daten in die eigene Datenbank zu pumpen.

Die Lizenzbedingungen der Google Maps sind recht verwirrend, dürften aber so zu verstehen sein, dass man die Daten nur in Verbindung mit der Anzeige einer Karte nutzen darf. Da ich lediglich die Distanz erhalten möchte und somit keine Verwendung für eine Karte habe, nutze ich für dieses Beispiel die Konkurrenz von Microsoft, Bing Maps.

Zunächst besorgen wir uns eine Windows Live ID im Bing Maps Account Center. Hier können wir unsere eigene E-Mail Adresse registrieren oder wir legen uns ein Hotmail Postfach an. Sind wir nun im Besitz eines Windows Live Zugangs, müssen wir uns nun den Account für die API erstellen. Sobald dies geschehen ist, finden wir im Menü unter „Create or view keys“ endlich die Möglichkeit, unseren Key zu erzeugen. Heraus kommt ein 64stelliger String, der so aussehen sollte (Beispiel-Key, zum Testen muss ein korrekter Key verwendet werden):
eN_U1StXBLh5N5mreI7RtdNXBS0_Ju9mclnMRNoNGyVkNeS7RcYRwVcP9NMCZMlS

Um nun eine Strecke von A nach B zu berechnen, benötigen wir nur noch die folgende URL:
http://dev.virtualearth.net/REST/v1/Routes?wp.1=%5BWegpunkt 1]&wp.2=[Wegpunkt 2]&key=[API Key]

Nehmen wir nun an, wir möchten von Gschlachtenbretzingen nach Michelbach an der Bilz fahren. Testen wir doch einfach einmal diese URL:
http://dev.virtualearth.net/REST/v1/Routes?wp.1=Gschlachtenbretzingen&wp.2=Michelbach an der Bilz-Gschlachtenbretzingen&key=eN_U1StXBLh5N5mreI7RtdNXBS0_Ju9mclnMRNoNGyVkNeS7RcYRwVcP9NMCZMlS

Das liefert uns einen riesigen Schwulst an Informationen im JSON Format. Irgendwo ziemlich am Ende versteckt sich die von uns gewünschte Information, die uns sagt, dass die travelDistance 3.158 km beträgt. Zwar ist das zurückgelieferte Format prädestiniert für die Weiterverarbeitung per JavaScript, doch wir möchten diese Informationen jetzt lieber im PHP haben, beispielsweise um diese gleich weiterzuverarbeiten.

<?php

// Die URL zum Dienst
$url = 'http://dev.virtualearth.net/REST/v1/Routes';

// Unser API Key
$key = 'eN_U1StXBLh5N5mreI7RtdNXBS0_Ju9mclnMRNoNGyVkNeS7RcYRwVcP9NMCZMlS';

// Alle Wegpunkte in einem Array. Koennen bis zu 25 sein. Es werden hier 
// verschiedene Moeglichkeiten der Angabe gezeigt
$a_wp = array(
	'Gschlachtenbretzingen',
	'Michelbach an der Bilz, BW, Deutschland',
	'74538 Rosengarten'
);

// Wie viele Wegpunkte haben wir im Array?
$anz_wp = count($a_wp);

// Erweitern der URL um die Wegpunkte
$url = $url.'?';
for($i = 0; $i < $anz_wp; $i++){
	$url .= 'wp.'.($i+1).'='.urlencode($a_wp[$i]).'&';
}

// Nun noch den API Key an die URL anhaengen
$url .= 'key='.$key;

// Wir holen uns die Daten vom Bing Server
$json_datei = file_get_contents($url);

// Der Server liefert die Daten im JSON Format, das ist eigentlich fuer
// Javascript gedacht. Deshalb muessen wir dieses umwandeln
$daten = json_decode($json_datei);

// Pruefen, ob wir die Daten auch korrekt erhalten haben
if ($daten->{'authenticationResultCode'} == 'ValidCredentials'){
	// Formatiere die Strecke
	$strecke =  number_format($daten->{'resourceSets'}[0]->{'resources'}[0]->{'travelDistance'}, 1, ',' , '.');
	// Formatiere die Dauer fuer die Fahrt
	$dauer = number_format(ceil($daten->{'resourceSets'}[0]->{'resources'}[0]->{'travelDuration'} / 60), 0, ',', '.');

	// Ausgabe unserer Informationen
	echo '<p>Die Strecke ';
	for($i = 0; $i < $anz_wp; $i++){
		echo $a_wp[$i];
		if($i+1 < $anz_wp) echo ' -> ';
	}
	echo ' beträgt '.$strecke.' Kilometer. Die Fahrt dürfte ohne Stau etwa '.$dauer.' Minuten dauern.</p>';
}

// Testausgabe mit allen erhaltenen Daten vom Bing Server zur Info
echo '<hr />';
echo '<p>Die zusammengesetzte URL: <a href="'.$url.'">'.$url.'</a></p>';
echo '<pre>';
print_r($daten);
echo '</pre>';

?>

Die Ausgabe sieht in diesem Fall wie folgt aus. In unserem Ergebnis sind noch viele weitere interessante Informationen versteckt. Ein genauerer Blick in die $daten kann sich also durchaus lohnen.

Die Strecke Gschlachtenbretzingen -> Michelbach an der Bilz, BW, Deutschland -> 74538 Rosengarten beträgt 12,1 Kilometer.
Die Fahrt dürfte ohne Stau etwa 17 Minuten dauern.