Während ich in diesem Artikel ein paar Erkenntnisse über die offizielle Fritz!Box-API festgehalten hatte, auf welche ich via PHP zugegriffen habe, möchte ich hier noch einen weiteren Weg nennen, der direkt auf das Web-UI der Fritz!Box zugreift, wie man sie selbst im Browser öffnet.

Auf diesem Wege kann man ggf. noch ein paar weitere Dinge abgreifen, die über die offizielle SOAP-Schnittstelle nicht möglich sind (ich vermisse dort zum Beispiel das gesammte Ereignis-Log und habe Probleme mit der Download-Statistik), allerdings stellt sich die Frage, ob der Zugriff nicht bei irgendeinem Update entfällt...

Login

Die erste Hürde ist das Login. Man macht zuerst eine neutrale Anfrage an der Fritz!Box, sollte man eingeloggt sein, bekommt man die Session-ID zurück, falls nicht, ist die SID 0000000000000000.
Mit dabei in der Antwort findet man eine Challenge, welche man für die Generierung eines gehashten Passworts benötigt.

IP-Adresse der Fritz!Box ist im Folgenden die Standard-Adresse 192.168.178.1, ich verwende das einfache Login, wie beim Web-UI mit nur einem Passwort (ohne Benutzername), es ist allerdings auch ein Login mit Benutzername/Passwort möglich, sollte man unter System > FritzBox-Benutzer Benutzer mit den entsprechenden Rechten angelegt haben.

curl http://192.168.178.1/login_sid.lua
wget -q -O - http://192.168.178.1/login_sid.lua # Wer wget lieber hat...

Antwort ist zum Beispiel

<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
  <SID>0000000000000000</SID>
  <Challenge>4640f673</Challenge>
  <BlockTime>0</BlockTime>
  <Rights></Rights>
</SessionInfo>

SID ist 0000000000000000, ich bin also nicht eingeloggt.
In BlockTime ist zu sehen, wie viele Sekunden ich noch für das Login blockiert bin, sollte ich mich eben mit einem falschen Passwort angemeldet haben.
Die Challenge 4640f673 benötigen wir!

Um sich erfolgreich einzuloggen, müssen wir an dieselbe Adresse, die wir eben verwendet hatten, noch als POST die response mitsenden, welche aus der Challange, gefolgt von einem Bindestrich und dem Passworthash besteht und den username, welcher entweder leer ist (wenn Login nur mit Passwort) oder eben noch den Usernamen beinhaltet.
Das Passworthash wird wie folgt erstellt:

echo -n "<Challenge>-<Passwort>" | iconv --from-code=UTF-8 --to-code=UTF-16LE | md5sum | sed  -e 's/ .*//'

# Als Beispiel:
echo -n "4640f673-test123" | iconv --from-code=UTF-8 --to-code=UTF-16LE | md5sum | sed  -e 's/ .*//'
# => Ergebnis: 1b8a76233fe8a89ffe908a9ea8c63410

Ein erfolgreiches Login mit curl oder wget und ohne Benutzernamen sollte dann wie folgt aussehen:

curl --data  "response=4640f673-1b8a76233fe8a89ffe908a9ea8c63410&username=" http://192.168.178.1/login_sid.lua
wget -q -O - --post-data="response=4640f673-1b8a76233fe8a89ffe908a9ea8c63410&username=" http://192.168.178.1/login_sid.lua # Wer wget lieber hat...

Und eine erfolgreiche Antwort:

<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
  <SID>897143e84d3d4ba4</SID>
  <Challenge>4640f673</Challenge>
  <BlockTime>0</BlockTime>
  <Rights> ...Infos über Zugriffsrechte... </Rights>
</SessionInfo>

Mit der zurückgegebenen SID können wir weitere Anfragen starten.
Ist die SID 0000000000000000 hat hier etwas nicht funktioniert und die BlockTime wird wahrscheinlich größer Null sein.

Ereignislog abgreifen

Über die TR-064-Schnittstelle bekomme ich hier leider nur einen Bruchteil der Ereignisse, deshalb eine für mich wichtige Abfrage:

# Zumindest mit FritzOS 7.12:
wget -q -O - --post-data="sid=897143e84d3d4ba4&lang=de&page=log&no_sidrenew=" http://192.168.178.1/data.lua

# Zwei Anhaltspunkte für ältere Firmwares
# (1) http://192.168.178.1/system/syslog.lua?tab=aus&event_filter=0&stylemode=print&sid=xxxxxxxxx
# (2) http://192.168.178.1/cgi-bin/webcm
#   mit folgener POST-Data:
#   getpage=../html/de/system/ppSyslog.html&logger:settings/filter=0&sid=xxxxxxxx

Die Ereignisse kommen als schönes JSON zurück (bei FritzOS 7.12).
Leider bin ich hier mit curl nicht zum gewünschten Ergebnis gekommen. Irgendwas ist da schief gelaufen...

Daten des Online-Zählers abgreifen

Daten der Seite Internet > Online-Monitor > Online-Zähler (Wie viele Daten habe ich heute/gestern/letzet Woche/... Hoch-/heruntergeladen?).
Über die TR-064-Schnittstelle bekomme ich hier leider sehr seltsame Werte, deshalb eine für mich wichtige Abfrage:

# Zumindest mit FritzOS 7.12:
wget -q -O - --post-data="sid=897143e84d3d4ba4&lang=de&page=netCnt&no_sidrenew=" http://192.168.178.1/data.lua

Hier bekomme ich den Inhalt der Seite als HTML-Quelltext zurück. Dies bedarf natürlich einiger RegEx-Suchen und könnte nach einem Firmware-Update unter Umständen nicht mehr wie gewünscht funktionieren.

Kann ich das auch mit PHP machen?

Klar!
Hier eine PHP-Klasse von mir. Ich lege allerdings nicht meine Hand dafür ins Feuer, dass diese Klasse einwandfrei funktioniert, sollte eine andere Firmware als FRITZ!OS 07.12 installiert sein!

Die Klasse regelt das Login und kann das Ereignis-Log und den Online-Zähler abrufen.
Mehr ist bisher nicht vorgesehen.

fritz_ui.php
<?php

class fritzbox_ui {
 
  protected $ip   = null;
  protected $sid  = null;
 
  function __construct($fb_ip = null, $fb_pass = null, $fb_user = null) {
    $this->login($fb_ip, $fb_pass, $fb_user);
    }
 
  function login($fb_ip = null, $fb_pass = null, $fb_user = null) {
    if ($fb_ip == null || $fb_pass == null)
      return false;
    $this->ip = $fb_ip;
   
    $ch = curl_init("http://".$this->ip."/login_sid.lua");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);

    $xml = new SimpleXMLElement($response);
    curl_close($ch);
    if ($xml->SID != 0) { $this->sid = $xml->SID; return true; } // Wir sind wohl schon eingeloggt

    $challange = $xml->Challenge;
    $pass_hash = md5(iconv('UTF-8', 'UTF-16LE', $challange."-".$fb_pass));
   
    $ch = curl_init("http://".$this->ip."/login_sid.lua");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, "response=$challange-$pass_hash&username=$fb_user");
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);
   
    $xml = new SimpleXMLElement($response);
    curl_close($ch);
    if ($xml->SID != 0) {
      $this->sid = $xml->SID;
      return true;
    } else {
      $this->sid = null;
      return false;
      }
    }
 
  function getEventLog($filter='') { // Filter: '': alles, 1: System, 2: Internetverbindung, 3: Telefonie, 4: WLAN, 5: USB-Geräte
    if ($this->sid == null) return false;
    $query = "sid=".$this->sid."&lang=de&page=log&xhrId=$filter&no_sidrenew=";
    if ($filter != "") $query .= "&filter=$filter&useajax=1";
    $ch = curl_init("http://".$this->ip."/data.lua");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);

    $json = json_decode($response, true);
    curl_close($ch);
    $out  = array();
    foreach ($json['data']['log'] as list($datum, $zeit, $log, $code)) {
      $datum = preg_replace("/(\d+)\.(\d+)\.(\d+)/", "$1.$2.20$3", $datum); // Aus dem Y2K-Bug wohl nix gelernt...
      $out[] = array("datum" => $datum, "zeit" => $zeit, "timestamp" => strtotime("$datum $zeit"), "log" => $log, "code" => $code);
      }
    return $out;
    }
 
  function getTrafficCounter() {
    if ($this->sid == null) return false;
    $query = "sid=".$this->sid."&lang=de&page=netCnt&no_sidrenew=";
    $ch = curl_init("http://".$this->ip."/data.lua");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);
    curl_close($ch);
    // pre Fritz.OS 7.28
    //if (preg_match_all("/<td datalabel=.Datenvolumen gesendet\(MB\). class=.vol.>(\d+)<\/td>/",  $response, $m)) $gesendet = $m[1];
    //if (preg_match_all("/<td datalabel=.Datenvolumen empfangen\(MB\). class=.vol.>(\d+)<\/td>/", $response, $m)) $empfangen = $m[1];
    //return array("heute"    => array("gesendet" => $gesendet[0], "empfangen" => $empfangen[0]),
    //       "gestern"  => array("gesendet" => $gesendet[1], "empfangen" => $empfangen[1]),
    //       "woche"    => array("gesendet" => $gesendet[2], "empfangen" => $empfangen[2]),
    //       "monat"    => array("gesendet" => $gesendet[3], "empfangen" => $empfangen[3]),
    //       "vormonat" => array("gesendet" => $gesendet[4], "empfangen" => $empfangen[4]));

    // Fritz.OS 7.28
    $json = array();
    if (preg_match("/const data =(.*);/", $response, $m)) $json = json_decode($m[1], true);
    return array("heute"    => array("gesendet"  => $this->_combineBytes($json['Today']['BytesSentHigh'],      $json['Today']['BytesSentLow']),
                     "empfangen" => $this->_combineBytes($json['Today']['BytesReceivedHigh'],     $json['Today']['BytesReceivedLow'])),
           "gestern"  => array("gesendet"  => $this->_combineBytes($json['Yesterday']['BytesSentHigh'],     $json['Yesterday']['BytesSentLow']),
                      "empfangen" => $this->_combineBytes($json['Yesterday']['BytesReceivedHigh'], $json['Yesterday']['BytesReceivedLow'])),
           "woche"    => array("gesendet"  => $this->_combineBytes($json['ThisWeek']['BytesSentHigh'],     $json['ThisWeek']['BytesSentLow']),
                      "empfangen" => $this->_combineBytes($json['ThisWeek']['BytesReceivedHigh'],  $json['ThisWeek']['BytesReceivedLow'])),
           "monat"    => array("gesendet"  => $this->_combineBytes($json['ThisMonth']['BytesSentHigh'],     $json['ThisMonth']['BytesSentLow']),
                      "empfangen" => $this->_combineBytes($json['ThisMonth']['BytesReceivedHigh'], $json['ThisMonth']['BytesReceivedLow'])),
           "vormonat" => array("gesendet"  => $this->_combineBytes($json['LastMonth']['BytesSentHigh'],     $json['LastMonth']['BytesSentLow']),
                      "empfangen" => $this->_combineBytes($json['LastMonth']['BytesReceivedHigh'], $json['LastMonth']['BytesReceivedLow']))
           );
    }
 
  // Rufumleitung einrichten
  // $sipNr: 0,1,2,3... Durchnummeriert wie in "Eigene Rufnummern" zu sehen
  // $nr: Die Telefonnummer, zu der umgeleitet werden soll
  // $typ: Sofort:          NOW    (Standard)
  //       Verzögert:       DELAY
  //       Verzögert lang:  DELAY_LONG
  //       Parallelruf:     PARALLEL
  function setDeflection($sipNr, $nr, $typ="NOW") {
    if ($this->sid == null) return false;
    $query  = "xhr=1&sid=".$this->sid."&lang=de&page=rul_edit&apply=";
    $query .= "callsToBeRedirected=call_to&listTo=SIP$sipNr&callsDestination=num&listNum=new_num&num_to=$nr&ownNumber=*&kind_msn=$typ&back_to_page=callRedi&uid=new&connectTo=$nr";
    $ch = curl_init("http://".$this->ip."/data.lua");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);
    curl_close($ch);
    }

  // Rufumleitung entfernen
  // $nr: Nummer der Rufumleitungsregel. Erste angelegte Regel hat die Nummer 0
  function removeDeflection($nr) {
    if ($this->sid == null) return false;
    $query  = "xhr=1&sid=".$this->sid."&lang=de&graphState=1&rul_$nr=1&TamNr=nil&back_to_page=/fon_num/rul_list.lua&popup_url=popup_url&delete=rul_$nr&oldpage=/fon_num/rul_list.lua";
    $ch = curl_init("http://".$this->ip."/data.lua");
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
    }

  function _combineBytes($high, $low) {
    return ($high * 4294967296 + $low)/1024/1024/1024;
    }
  }
?>

Und wie soll ich das verwenden?

<?php

include 'fritz_ui.php';

// Login nur mit WebUI-Passwort
//$f = new fritzbox_ui("192.168.178.1", "mein-passwort");

// Achtung: Ab Fritz.OS 7.28 (oder schon ein bisschen früher), gibt es NUR noch
// das folgende Login MIT Benutzername und Passwort, auch wenn man beim Login
// im Browser selbst nur das Passwort eingeben muss!
// Es gibt einen Benutzer 'fritzXXX' im Hintergrund, den man als Benutzernamen
// verwenden muss, wenn man dieses Skript verwendet.
// Der Benutzername wurde automatisch von der Fritz!Box erstellt und
// ist im Web-Interface unter System > Fritz!Box-Benutzer zu finden

// Login mit Benutzername und Passwort
$f = new fritzbox_ui("192.168.178.1", "mein-passwort", "apfelz-user");

// Ereignis-Log ausgeben
print_r($f->getEventLog());

// Nur eine Kategorie des Event-Logs ausgeben
// $filter => 1: System, 2: Internetverbindung, 3: Telefonie, 4: WLAN, 5: USB-Geräte
print_r($f->getEventLog($filter);

// Online-Zähler ausgeben
print_r($f->getTrafficCounter());

// Rufumleitung für 1. SIP-Leitung zur Nummer 01805554711 einrichten
$f->setDeflection(0, "01805554711");


?>