PHP Script "verstolpert sich manchmal"


Sprint

Erfahrenes Mitglied
#1
Hallo zusammen,

ich bin gerade dabei, ein Programm zu schreiben um einige hunderttausend Dateien zu verschlüsseln. Nun ist das Problem, daß das Script MANCHMAL ins Stolpern gerät und teilweise totalen Mist produziert und manchmal fehlerfrei durchläuft.

Zum Ablauf: Zuerst werden in eine Tabelle aller vorhandenen Dateien geschrieben. Dabei werden ungültige Dateinamen bereinigt und die neuen Namen ebenfalls in die Tabelle eingetragen. Nun geht das Script die Tabelle durch, nimmt eine Datei, verschlüsselt sie und entschlüsselt sie anschließend in eine temporäre Datei, um mittels SHA1 Prüfsummen die korrekte Verschlüsselung zu prüfen. Wenn korrekt, wird die Prüfsumme in eine separate Tabelle eingetragen, der ursprüngliche Datensatz als erledigt gekennzeichnet und die offene Datei gelöscht. Wenn die Verschlüsselung fehlgeschlagen ist, werden insgesamt drei Versuche gestartet, bevor der Eintrag entsprechend gekennzeichnet wird und der nächste Datensatz aufgerufen wird.

PHP:
$anfang = time();
$kmerker = '';
$sql = "select * from encfiles where erledigt = 0 order by kdnr";
$erg = mysqli_query($mysqli, $sql);        //Datanbankanfrage senden
if (!$erg)
    die('<font color="red">Datenbankfehler: '.__FILE__.' Zeile '.__LINE__);
$fp = fopen('errorlog.txt', 'a');
while ($zeile = mysqli_fetch_array($erg, MYSQLI_ASSOC)){
    if ($kmerker != $zeile['kdnr']){
        $kmerker = $zeile['kdnr'];
        $sqlc = "select k1, k2 from kunden where kdnr = '".$zeile['kdnr']."'";
        $ergc = mysqli_query($mysqli, $sqlc);        //Datanbankanfrage senden
        $zeilec = mysqli_fetch_array($ergc, MYSQLI_ASSOC);
        $key1 = secured_decrypt_data($zeilec['k1'], '', '');
        $key2 = secured_decrypt_data($zeilec['k2'], '', '');
    }
    $pathparts = pathinfo($zeile['neuname']);
    $upname = $pathparts['dirname'].'/'.strtoupper($pathparts['basename']);
    $neuname = $upname.'.enc';
    $orgsha = sha1_file($zeile['datei']);
    $verschluesselt = false;
    $versuche = 0;
    while (!$verschluesselt && $versuche < 3){
        if (file_exists($zeile['datei'])){
            $enc_file = encryptFile($zeile['datei'], $key2, $neuname);

            decryptFile($neuname, $key2, 'temp/testfile.nix');
            $decsha = sha1_file('temp/testfile.nix');
            if ($orgsha == $decsha){
                $sqlw = "update encfiles set erledigt = 1 where lnr = '".$zeile['lnr']."';";
                $ergw = mysqli_query($mysqli, $sqlw);

                $sqlw = "insert into filesha (datei, sha) value ('$upname', '$orgsha')";
                $ergw = mysqli_query($mysqli, $sqlw);
                fwrite($fp, date('H:i:s')."\t".$orgsha."\t".$decsha."\t".$zeile['datei']."\t ok \r\n");

                unlink($zeile['datei']);
                unlink('temp/testfile.nix');
                $verschluesselt = true;
            }else{
                if ($orgsha == '')
                    $orgsha = "\t\t\t\t\t";
                if ($decsha == '')
                    $decsha = "\t\t\t\t\t";
                fwrite($fp, date('H:i:s')."\t".$orgsha."\t".$decsha."\t".$zeile['datei']."\t fehler \r\n");
                unlink($neuname);
                unlink('/var/www/vhosts/herpolsheimer.ag/pruefung.herpolsheimer.ag/temp/testfile.nix');
                if (time() - $anfang > 110){
                    fclose($fp);
                    die();
                }
                $versuche++;
            }
        }else{
            fwrite($fp, date('H:i:s')."\t\t\t\t\t\t\t\t\t\t\t\t\t".$zeile['datei']."\t Datei fehlt \r\n");
            $sqlw = "update encfiles set erledigt = 3 where lnr = '".$zeile['lnr']."';";
            $ergw = mysqli_query($mysqli, $sqlw);
            $verschluesselt = true;
        }
    }
    if (!$verschluesselt && $versuche == 3){
        fwrite($fp, date('H:i:s')."\t".$orgsha."\t".$decsha."\t".$zeile['datei']."\t abbruch \r\n");
        $sqlw = "update encfiles set erledigt = 2 where lnr = '".$zeile['lnr']."';";
        $ergw = mysqli_query($mysqli, $sqlw);
        unlink('temp/testfile.nix');
    }
    if (time() - $anfang > 110){
        fclose($fp);
        die();
    }
}
Theoretisch für mich ein einfacher und normalerweise problemloser Ablauf. Doch jetzt kommt das Problem. Es KANN passieren, daß das Script sich scheinbar selbst überholt. Dann wird eine Datei korrekt verarbeitet, dann aber nochmal aufgerufen. Blöd bei dieser Situation ist, daß dann sowohl Ausgangsdatei als auch verschlüsselte Datei gelöscht sind. Oder es wird drei Mal verkehrt verschlüsselt, dann aber die Datei nochmal aufgerufen und dieses mal korrekt verschlüsselt.

Merkwürdig ist auch, daß Datensätze erneut aufgerufen werden, obwohl sie bereits als verarbeitet gekennzeichnet wurden. Eben als ob das Script mehrfach laufen würde und sich gegenseitig überholt. Zeitweise geht es gut und zeitweise eben nicht. Dafür würde auch sprechen, daß die Fehler nicht konstant durchgehen, sondern zwischendrin auch wieder hunderte und tausende Dateien korrekt im ersten Lauf verschlüsselt werden. Das Programm wird aber nicht mehrfach gestartet.

Es kann aber auch sein, daß eine Stunde später der selbe Lauf mit den exakt selben Dateien ohne einen einzigen Fehler durchläuft.

Ich hatte auch schon die Überlegung, daß es an der Sortierung der Datensätze liegen könnte. Ich lasse sie nach Kundennummern sortiert laufen, um nicht ständig den individuellen Schlüssel auslesen zu müssen. Könnte das das Problem sein? Wäre es besser, die Reihenfolge mit Zufallszahlen durcheinander zu bringen um das Script damit einzubremsen?

Auch mit dem Support unseres Providers bin ich die Sache schon ein paar mal durchgegangen. Er hat an ein paar Servereinstellungen gedreht, was aber auch nichts gebracht hat.

Im Moment sind es nur kleine Testläufe mit 30- 50000 Dateien. Nur wenn es dann mal soweit ist und die originalen Dateien drankommen, dann darf es keine Fehler geben. Aus Platzgründen kann ich die rund 200 GB an Daten nicht aufheben bis alles erledigt ist. Ich muß die nach Verarbeitung löschen.

Hat jemand eine Idee, wie das Programm optimiert werden kann oder so eingebremst, daß es nicht ins stolpern kommt? Beim zeitlichen Rahmen ist noch Luft. Die rund 200000 Dateien brauchen zum verschlüsseln geschätzt 4-5 Stunden, wenn es also etwas länger dauert, wäre das nicht das Problem. Es muß nur innerhalb eines Tages durchgelaufen sein.

Vielen Dank schon mal im Voraus,
Sprint
 

ComFreek

Mod | @comfreek
Moderator
#2
Hallo Sprint,

deine while-Schleife mit file_exists-Prüfung wirkt sehr verdächtig. Allgemein ist eine Prüfung, ob eine Datei existiert, bevor sie sowieso geöffnet wird (wie bei dir durch encryptFile), immer verdächtig:
PHP:
if (file_exists($filename)) {
  ... fopen($filename); ...
}
else {
  // Failure
}
Zwischen "{" des ifs und "fopen" kann die Datei auch gelöscht werden. Auch garantiert dir file_exists nicht, dass du die Datei auch öffnen kannst (u. a. wegen fehlender Rechte). Die beste Lösung ist es, einfach direkt fopen aufzurufen und dann den Fehlercode zu überprüfen.

Zurück zu deinem Problem: insbesondere würde ich ein Auge auf folgende mögliche Sequenz von Codezeilen in deinem Programm werfen:
PHP:
unlink($zeile['datei']);      // In vorheriger Iteration gelöscht, oder eher: Auftrag zur Löschung ans OS abgesetzt
// ...
file_exists($zeile['datei']) // In aktueller Iteration könnte das vielleicht noch true liefern
(Die Zeilen stehen so nicht hintereinander in deinem Code, aber wenn die while-Schleife min. 2 Mal ausgeführt wird, dann werden diese zwei genau in der Reihenfolge ausgeführt.)

Meine Empfehlung wäre daher: Verlass dich nicht aufs Dateisystem, um deine Businesslogik auszuführen. Ob du in vorheriger Iteration unlink aufgerufen hast, sollte sich eine boolesche Variable merken und nicht das Dateisystem. Außerdem würde ich den Code in mehrere Unterfunktionen splitten, so lässt es sich leichter auf Korrektheit beurteilen. SQL Transaktionen könnten auch hilfreich sein. Wenn dein Skript nämlich einmal abbricht, ist deine DB in einem womöglich inkonsistenten Zustand!

$sqlw = "insert into filesha (datei, sha) value ('$upname', '$orgsha')";
Pass hier übrigens auf mit SQL Injection (wenn auch nicht mutwillige)! Ich würde Prepared Statements empfehlen - aber außerhalb der Schleife prepare aufrufen. Das sollte auch die Geschwindigkeit deines Skripts erhöhen, da das SQL nur einmal geparst werden muss.
 

Sprint

Erfahrenes Mitglied
#3
Hallo ComFreek,

die while Schleife ist drin, da ich eben festgestellt hatte, daß sowohl bei Ver- als auch Entschlüsselung Fehler gemacht werden. Darum ja auch die anschließende Entschlüsselung, wobei korrekterweise die auch drei Mal laufen müßte, um eventuelle Entschlüsselungsfehler zu umgehen.

Das folgende file_exists soll verhindern, daß durch einen solchen Stolperer eine korrekt verschlüsselte Datei wieder gelöscht wird, weil die Prüfsummen nicht übereinstimmen. Aber das scheint ja dann nicht so abzulaufen wie gewollt.

Das ganze Script muß keinen Schönheitspreis gewinnen. Es wird später nur ein einziges Mal laufen, wenn eben die bis jetzt offenen Dateien verschlüsselt werden. Anschließend wird es ja wieder gelöscht. Darum sehe ich hier auch kein SQL Injection Problem. Alle Dateinamen werden vorher überprüft und rigoros gefiltert. Alles was nicht Zahl, Buchstabe oder ein paar Sonderzeichen ist, fliegt raus.

Aber gut, dann werde ich nächste Woche das Ding nochmal überarbeiten und erst mal die ständigen Löschungen rausnehmen. Vielleicht baue ich das auch so um. daß alles in 10 oder 20000er Blöcken gemacht wird. Dann reicht der Platz, um die originalen Dateien zu behalten und sie erst nach einer Überprüfung der Verschlüsselung zu löschen.

Auf jeden Fall schon mal vielen Dank für die Denkanstöße.