Dieses Informationsfenster platziere ich freiwillig und unentgeldlich zur Unterstützung meiner Schwester bei ihrem Jobwechsel. Es wird nur für eine begrenzte Zeit angezeigt und kann ausgeblendet werden. Ich hoffe, dass die Aktion keinen meiner Leser zu sehr stört. Zum Ausblenden werden Cookies benutzt, diese müssen aktiviert und für diese Seite erlaubt werden. Werden die Cookies gelöscht oder die Seite auf einem anderen Gerät betrachtet, erscheint das Informationsfenster wieder und muss erneut geschlossen werden.

Information

Katja Schurig sucht neue berufliche Herausforderungen!

Liebe Leser, ich habe hier einen Geheimtipp für euch:
Katja Schurig
Katja Schurig, berufserfahren und kampferprobt in den Bereichen Veranstaltungsmanagement und -verkauf, Personalwesen, Sales & Marketing sowie Tourismus, will sich neuen beruflichen Herausforderungen stellen. Wer mit viel Energie umgehen kann und auf der Suche nach einer professionellen Fachkraft ist (oder jemanden kennt, der jemanden kennt), der findet ihr Profil nebst Kontaktdaten auf: Xing
Edit Comment
IT-Stuff, Tutorials, Software, Internet, Unterhaltung Willkommen auf IMA

Archive for the ‘Programmierung’ Category

Backup-Lösung für FTP und MySQL in PHP

Dieser Beitrag ist eine Ergänzung bzw. Erweiterung des vorherigen Posts „FTP-Backup-Lösung mit PHP“. Die große Neuerung der Version 1.2 ist die Funktion MySQL Datenbanken sichern zu können. Auch hier wird ein Backup erstellt, überschüssige Backups (wenn mehr vorhanden sind als der gewünschte Maximalwert) werden gelöscht und neue Backups ggf. zu einem externen Server übertragen. Mit Version 1.2.1 gibt es zusätzlich die Möglichkeit, ALLE Ordner der Root-Ebene, mit Ausnahmen, zu sichern und Version 1.2.2 ermöglicht detailliertere Ausnahmen.

Features

Diese Lösung (v1.2.2) bietet nun folgenden Funktionsumfang:

  • beliebig viele Ordner des All-Inkl Accounts in einzelne .tar.gz Archive sichern
  • oder: alle Ordner der Root-Ebene, mit möglichen Ausnahmen, sichern
  • Detailliertere Ausnahmen mit Datei- und Ordnermasken wie z.B. „*.tar.gz“
  • Einschränkung der Anzahl aufgehobener Backups – älteste Backups werden automatisch gelöscht
  • detaillierte Ausgabe inklusive benötigter Zeit
  • E-Mail Benachrichtigung
  • Farbliche Hervorhebung
  • Verbesserungen des Backup Prozesses, zusätzliche Überprüfungen und Debug Infos bei Fehlern
  • Verbinden eines externen FTP Server und Kopieren aller neuen Backups
  • Angabe eines beliebigen FTP Ports
  • Verbindung über FTPs (SSL FTP) Port 21 wird verwendet, unsicheres FTP nur noch als Fallback
  • detailliertere Informationen über die Backups in der Benachrichtigungsmail
  • Backup von beliebig vielen MySQL Datenbanken von localhost, Aufräumen der Backups und Export an externen Server
  • E-Mail Anpassungen über Parameter möglich – Betreff, Anmerkungen, Details
  • ausführliche Ausgabe aller Backups im Skript und per Mail

Zwischen den Zeilen 37 und 82 findet ihr alle Variablen, die ihr anpassen müsst/könnt.

Zur Datenbanksicherung ist zu sagen, dass diese auf den Hoster All-Inkl optimiert ist. Sie sichert nur Datenbanken von localhost und benötigt den PHP Befehl „exec()“ sowie die Komponenten „mysqldump“ und „gzip“, die auf All-Inkl Servern erlaubt bzw. installiert sind. Auf anderen Hostern müssen daher ggf. diese Möglichkeiten geschaffen oder die MySQL Sicherung (Zeile 190-191) verändert werden.

Screenshot

Das Bild zeigt die Ausgaben des Backup Skripts und die versendete E-Mail Benachrichtigung

Code

Schaut für Code-Alternativen oder ein weniger komplexes System auch auf die Version 1.1 und 1.0

Update 03.2016: Update des Codes auf Version 1.2.2
Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

<?
/*
###############################
###############################
###
### Title: FTP & MySQL Backup Script
### Author: Hannes Schurig
### Date: 03.2016
### Version: 1.2
### Changelog: 
### 1.0: http://hannes-schurig.de/09/05/2015/ftp-backup-skript-in-php/
### 1.1: http://hannes-schurig.de/15/05/2015/ftp-backup-loesung-mit-php/
### 1.2: http://hannes-schurig.de/09/06/2015/backup-loesung-fuer-ftp-und-mysql-in-php/
### 1.2.1: new: backup ALL root subfolders, backup ALL root with excludes
### 1.2.2: issue fixed, added one more exclude parameter for files and folders
###
###############################
###############################
*/

// if no errors are shown, please check htaccess restrictions by "php_flag display_errors off"
// in this or parent folders
@error_reporting(E_ALL);
@ini_set("max_execution_time", 300);
@ini_set("memory_limit", "256M");
header('Content-Type: text/html; charset=utf-8');
include "Archive/Tar.php";
$pfad = preg_replace('/(\/www\/htdocs\/\w+\/).*/', '$1', realpath(__FILE__));
$allFtpTime = 0;
$allSqlTime = 0;
$newFtpBackups = array();
$newSqlBackups = array();
$backupinfo = array();
$mailText = "";
echo "<style>.ok{color:#478F47;}.err{color:#DA3833;}.grey{color:grey;}.warn{color:#f0ad4e;}th,td{border-bottom: 1px solid #aaa;}</style>";

// ########## EDIT HERE ###################
// ### FTP Data
  // backup ALL root folders:
  // if backupAllRootFolders is 1, ftpFoldersToBackup will be ignored and all root folders will be backed up
  $backupAllRootFolders = 0;
  // exclude specific root folders from ALL-BACKUP
  $excludeRootFolders = array("tech");

  // specific folder backup:
  // if backupAllRootFolders is 0, you can specify multiple folders in ftpFoldersToBackup to backup
  // which root folders should get backed up? comment is optional
  // format: "foldername___comment"
  // or: "foldername"
  $ftpFoldersToBackup = array("reports", "tools___selfhosted seo tools");

  // exclude files or folders - you should keep these four entries and add your stuff
  $ignoreMore = array("*.sql.gz", "*.tar.gz", "usage", "logs");

// ### FTP Export:
  $copyToExternalFtp = 1; // copy new backup files to external ftp server? should be 1/"yes"/"ja" or 0/"no"/"nein"
  // external (ftp) servers to copy new backups to, format:
  // in general: ftp://username:password@url:port/path (port is required!)
  // ftp://user:pw@ftp.server.com:21/
  // ftp://user:pw@serverurl.com:21/optional/path
  $externalFtpUri = "ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups";
// ### MySQL Data
  $backupMysqlData = 1;
  // one or more databases of the local mysql server to backup, multiple data combined with "___", comment is optional
  // format: "dbName___dbUser___dbPassword___comment"
  // or: "dbName___dbUser___dbPassword"
  $databasesToBackup = array(
  // two examples - please remove these
    "d01cedbName___d01cedbUser___dbPassword___beispielkommentar", 
    "d01d7dbName___d01d7dbUser___dbPassword"
  );
// ### Mailing Data
  $sendMail = 1; // send notification mail when all backups are done - should be 1/"yes"/"ja" or 0/"no"/"nein"
  $mailTo = "admin@yourdomain.com"; // valid mail address to send the mail to
  $mailSubject = "Automatische FTP Sicherung abgeschlossen"; // mail subject
  // additional mail annotations that gets inserted after the main mail content
  $mailAdditionalNotes = "Hiermit kann - wenn gewünscht - zusätzlicher Text in die Mail eingebaut werden.";
  $mailBackupDetails = 1; // send information about successful backup files? should be 1/"yes"/"ja" or 0/"no"/"nein"
// ### General Options
  $backupFileMaximum = 2; // how many archives should be stored?
  $dir = $pfad."backup/"; // in which subfolder is this backup php file? this would be: "root/backup/"
// ######### STOP EDITING HERE ###################

echo "<span class='ok'>".date("d.m.Y G:i:s")."</span><br><br>";
$allTime = time();

echo "<strong>_________________ FTP __________________</strong>";
// ###################
// ### FTP BACKUPS ###
// ###################
if ($backupAllRootFolders==1) {
  $ftpFoldersToBackup = array_filter(glob($pfad."*", GLOB_ONLYDIR));
  $ftpFoldersToBackup = preg_replace('/(\/www\/htdocs\/\w+\/)/', '', $ftpFoldersToBackup);
  if (isset($excludeRootFolders))
    $ftpFoldersToBackup = array_diff($ftpFoldersToBackup, $excludeRootFolders);
}
foreach ($ftpFoldersToBackup as $folderData) {
  // get data from folder string by splitting at "___"
  $arrThisJobData = explode("___", $folderData, 2);
  // check for minimal size of 1: folderName
  if(count($arrThisJobData)>=1) {
    $folder = $arrThisJobData[0];
    // optional: comment - could be empty
    if(empty($arrThisJobData[1])) {
      $folderComment = "";
    } else {
      $folderComment = $arrThisJobData[1];
    }

    $ftpJobTime = time();
    echo "<br>########################################<br>";
    echo "<strong>Verzeichnis $folder wird gesichert...</strong><br>";
    flush();

    // check if folder exists
    if(!file_exists($pfad.$folder)) {
      echo "<span class='err'>Sicherung fehlgeschlagen. Zu sichernder Ordner $pfad$folder existiert nicht.</span>";
      continue;
    }

    // Name: [verzeichnis]_[Datum]_[Uhrzeit].tar.gz
    $archivName = "FTP_$folder".date('_Y-m-d_His').".tar.gz";
    // Name: [All-Inkl-Accountname]_[Datum]_[Uhrzeit].tar.gz
    //$archivName = preg_replace('/.+\/(.+)\/$/', '$1', $pfad).date('_Y-m-d_His').".tar.gz";

    // ######### create backup
    $archiveObject = new Archive_Tar($archivName, true);
    $archiveObject->setIgnoreList($ignoreMore);
    $archiveObject->createModify($pfad.$folder, "", $pfad);

    // process backup
    $archivePath = $dir.$archivName;
    $archivSize = round(filesize($archivePath)/1000000, 1);
    
    // check created archive
    if(!validateBackup($archivePath)) {
      // abort process due to wrong type or too small filesize (which likely is an error)
      echo "<span class='err'>Verzeichnis-Sicherung fehlgeschlagen. Erstelltes Archiv ist fehlerhaft.</span><br>";
      // debug
      echo "<p class='grey'>Debug:<br>";
      echo "Pfad: $archivePath<br>";
      echo "Typ: ".gettype($archivePath)."<br>";
      echo "Größe: $archivSize MB</p>";
      continue;
    } else {
      $backupTime = time() - $ftpJobTime;
      if (is_int($backupTime)) {
        echo "<span class='ok'>Backup fertig: $archivName (Größe: $archivSize MB, Dauer: $backupTime Sekunden)</span><br>";
      } else {
        echo "<span class='ok'>Backup fertig: $archivName</span><br>";
      }
      $newFtpBackups[$archivName] = $archivSize." MB";
      array_push($backupinfo, array($folder, $archivName, $archivSize." MB", $backupTime." Sekunden", date("d.m.Y G:i:s"), $folderComment));
    }

    // ########## delete backups if too many
    echo "Aufräumen der FTP Backups...<br>";
    cleanBackups("FTP", $folder, $dir, "gz");
    
    // close job
    $ftpBackupEndTime = time() - $ftpJobTime;
    echo "<span class='ok'>Backup für Verzeichnis $folder abgeschlossen.</span><br>";
    if (is_int($ftpBackupEndTime)) {
      echo "######################################## (Dauer: $ftpBackupEndTime Sekunden)<br>";
    } else {
      echo "########################################<br>";
    }
    flush();
  } else {
    echo "<span class='err'>Angabe der Verzeichnis Daten fehlerhaft.</span>";
    echo "<p class='grey'>Debug:<br>";
    echo "Verzeichnis Name: $arrThisJobData[0]<br>";
    echo "Verzeichnis Comment: $arrThisJobData[1]<br>";
  }
}

// ########## echo backup summary and add it to mailtext
backupSummary($newFtpBackups);
flush();

echo "<strong>________________ MySQL ________________</strong>";
// ###################
// ### SQL BACKUPS ###
// ###################
if (!isset($backupMysqlData) || $backupMysqlData== 0 && in_array($backupMysqlData, array("no", "nein"))) {
  echo "<span class='warn'>MySQL Datenbanken werden nicht gesichert. Option ist deaktiviert.</span><br>";
} else {
  if(isset($databasesToBackup)&&count($databasesToBackup)>=1) {
    foreach ($databasesToBackup as $databaseData) {
      $sqlJobTime = time();
      // get data from database string by splitting at "___"
      $arrThisJobData = explode("___", $databaseData, 4);
      // check for minimal size of 3: dbName, dbUser, dbPassword
      if(count($arrThisJobData)>=3) {
        $dbName = $arrThisJobData[0];
        $dbUser = $arrThisJobData[1];
        $dbPassword = $arrThisJobData[2];
        // optional: comment - could be empty
        if(empty($arrThisJobData[3])) {
          $dbComment = "";
        } else {
          $dbComment = $arrThisJobData[3];
        }
        echo "<br>########################################<br>";
        echo "<strong>Datenbank $dbName wird gesichert...</strong><br>";
        flush();
        $sqlFile = "DB_$dbName".date('_Y-m-d_His').".sql";
        exec("mysqldump -u '$dbUser' -p'$dbPassword' --quick --allow-keywords --add-drop-table --complete-insert --quote-names '$dbName' >$sqlFile");
        exec("gzip $sqlFile");
        $sqlFilePath = $dir.$sqlFile.".gz";
        $sqlSize = round(filesize($sqlFilePath)/1000000, 1);
        if(!validateBackup($sqlFilePath)) {
          // wrong type or too small filesize (which likely is an error)
          echo "<span class='err'>Datenbank-Sicherung fehlgeschlagen. Erstellter Export ist fehlerhaft.</span><br>";
          // debug
          echo "<p class='grey'>Debug:<br>";
          echo "Datenbank Name: $dbName<br>";
          echo "User: $dbUser<br>";
          echo "Passwort: $dbPassword<br>";
          echo "Kommentar: $dbComment<br>";
          echo "Pfad: $sqlFilePath<br>";
          echo "Typ: ".gettype($sqlFilePath)."<br>";
          echo "Größe: $sqlSize MB</p>";
        } else {

          $backupTime = time() - $sqlJobTime;
          if (is_int($backupTime)) {
            echo "<span class='ok'>Backup fertig: $sqlFile (Größe: $sqlSize MB, Dauer: $backupTime Sekunden)</span><br>";
          } else {
            echo "<span class='ok'>Backup fertig: $sqlFile</span><br>";
          }
          $newSqlBackups[$sqlFile.".gz"] = $sqlSize." MB";
          array_push($backupinfo, array($dbName, $sqlFile, $sqlSize." MB", $backupTime." Sekunden", date("d.m.Y G:i:s"), $dbComment));
        }
        
        // ########## delete DB backups if too many
        echo "Aufräumen der DB Backups...<br>";
        cleanBackups("DB", $dbName, $dir, "gz");

        $sqlJobEndTime = time() - $sqlJobTime;
        echo "<span class='ok'>Backup für Datenbank $dbName abgeschlossen.</span><br>";
        if (is_int($sqlJobEndTime)) {
          echo "######################################## (Dauer: $sqlJobEndTime Sekunden)<br>";
        } else {
          echo "########################################<br>";
        }
        flush();
      } else {
        echo "<span class='err'>Angabe der DB Daten fehlerhaft.</span>";
        echo "<p class='grey'>Debug:<br>";
        echo "DB Name: $arrThisJobData[0]<br>";
        echo "DB User: $arrThisJobData[1]<br>";
        echo "DB PW: $arrThisJobData[2]<br>";
        echo "DB Comment: $arrThisJobData[3]</p><br>";
      }
    } // foreach
  } // if
}

// ########## echo backup summary and add it to mailtext
backupSummary($newSqlBackups);
flush();

echo "<strong>________________ Export _________________</strong>";
// ########################
// ### COPY TO EXTERNAL ###
// ########################
if (!isset($copyToExternalFtp) || $copyToExternalFtp== 0 && in_array($copyToExternalFtp, array("no", "nein"))) {
  echo "<span class='warn'>Backups werden nicht auf einen externen FTP kopiert. Option ist deaktiviert.</span><br>";
} else {
  $ftpTime = time();
  echo "<br>########################################<br>";

  // get a ftp connection resource
  $ftpCon = getFtpConnectionByURI($externalFtpUri);
  flush();

  // ########## copy all FTP backups to external FTP
  copyBackupsToFtp($newFtpBackups, "FTP", $ftpCon);
  flush();

  // ########## copy all DB/SQL backups to external FTP
  copyBackupsToFtp($newSqlBackups, "DB", $ftpCon);
  flush();

  ftp_close($ftpCon);
  $ftpExternalEndTime = time() - $ftpTime;
  array_push($backupinfo, array("Export -> external FTP", count($newSqlBackups) + count($newFtpBackups) . " Sicherungen", "", $ftpExternalEndTime." Sekunden", date("d.m.Y G:i:s"), ""));
  if (is_int($ftpTime)) {
    echo "######################################## (Dauer: $ftpExternalEndTime Sekunden)<br>";
  } else {
    echo "########################################<br>";
  }
}
flush();

// ###################
// ### PRINT INFOS ###
// ###################
echo "<br><strong>_____________ Zusammenfassung _____________</strong>";
$backupDetailsText = backupDetails($backupinfo);
echo $backupDetailsText;

// #################
// ### SEND MAIL ###
// #################
if (!isset($sendMail) || $sendMail== 0 && in_array($sendMail, array("no", "nein"))) {
  echo "<br><span class='warn'>Benachrichtigungsmail wurde nicht verschickt. Option ist deaktiviert.</span><br>";
} else {
  if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $mailTo)) {
    echo "<br><span class='err'>FEHLER: Mail konnte nicht versendet werden, da die Empfängeradresse ungültig ist!</span><br>";
  } else {
    if (empty($mailSubject)) $mailSubject = "Automatische FTP Sicherung abgeschlossen";
    // add backup informations as table to mailtext
    if (!isset($mailBackupDetails) || $mailBackupDetails== 0 && in_array($mailBackupDetails, array("no", "nein"))) {
      // do not append information about backups
    } else {
      $mailText = backupDetails($backupinfo);
    }
    if (!empty($mailAdditionalNotes)) $mailText .= "<br>Anmerkungen:<br>".$mailAdditionalNotes;
    mail(
      $mailTo,
      $mailSubject,
      $mailText,
      "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
    ) or die("<br><span class='err'>FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden.</span><br>");
    echo "<br><span class='ok'>Benachrichtigungsmail wurde erfolgreich verschickt!</span><br>";
  }
}
flush();

function backupDetails($backups) {
  global $allTime;
  $columnDefinition = array("Ordner/DB-Name", "Dateiname", "Größe", "Dauer", "Timestamp", "Kommentar");
  $backupDetailsText = "<br><table border=0 style='border-spacing:0;'><thead><tr></tr></thead><tbody>";
  for($d=0;$d<count($columnDefinition);$d++) {
    $backupDetailsText .= "<th style='padding:1px 13px 1px 0px;text-align:left;'>" . $columnDefinition[$d] . "</td>";
  } 
  $backupDetailsText .= "</tr></thead><tbody>";
  for($i=0;$i<count($backups);$i++) {
    $backupDetailsText .= "<tr>";
    for($j=0;$j<count($backups[$i]);$j++) {
      $backupDetailsText .= "<td style='padding:1px 13px 1px 0px;text-align:left;'>" . $backups[$i][$j] . "</td>";
    } 
    $backupDetailsText .= "</tr>";
  }
  $backupDetailsText .= "</tbody></table>";
  $allEndTime = time() - $allTime;
  if (is_int($allEndTime)) {
    $backupDetailsText .= "<strong>Gesamtbackupzeit:</strong> $allEndTime Sekunden<br>";
  }
  return $backupDetailsText;
}

function validateBackup($filePath) {
  if(!file_exists($filePath)) {
    echo "<span class='err'>Fehler bei der Backup Überprüfung. Die Backup-Datei ist fehlerhaft oder nicht vorhanden.</span><br>";
    return false;
  }
  if (!is_numeric(filesize($filePath))) {
    echo "<span class='err'>Fehler bei der Backup Überprüfung. Die Größe der Backup-Datei ist fehlerhaft.</span><br>";
    return false;
  }
  $backupFullSize = filesize($filePath);
  // check created dump
  if (!is_file($filePath)==1 || !$backupFullSize>50) {
    echo "<span class='err'>Fehler bei der Backup Überprüfung. Die Backup-Datei ist fehlerhaft.</span><br>";
    return false;
  } else {
    return true;
  }
}

function cleanBackups($dataTypeIdentifier, $nameIdentifier, $dir, $fileType) {
  global $backupFileMaximum;
  flush();
  // integer starts at 0 before counting
  $i = 0;
  $backupFiles = array();
  // ######### collect valid backup files
  if ($handle = opendir($dir)) {
    while (($file = readdir($handle)) !== false) {
      if (is_int(strpos($file, $dataTypeIdentifier."_".$nameIdentifier)) == true &&
            pathinfo($file)["extension"] == $fileType &&
            !in_array($file, array('.', '..')) &&
            !is_dir($dir.$file)
      ) {
        $backupFiles[$dir.$file] = filectime($dir.$file);
      }
    }
  }
  echo count($backupFiles)." valide Backups dieses Ordners gefunden, ";
  echo "$backupFileMaximum Backups sollen behalten werden. ";
  if (count($backupFiles)-$backupFileMaximum>0) { 
    echo count($backupFiles)-$backupFileMaximum;
  } else { 
    echo "0";
  }
  echo " Backups werden gelöscht:<br>";
  flush();

  // ######### sort and delete oldest backups
  // sort backup files by date
  arsort($backupFiles);
  // reset counter variable
  $i = 0;
  // delete oldest files
  foreach ($backupFiles as $filePath => $value) {
    if($i>=$backupFileMaximum) {
      echo "$filePath wird gelöscht...<br>";
      if (unlink($filePath)) {
        echo "<span class='ok'>Datei erfolgreich gelöscht.</span><br>";
      } else {
        echo "<span class='err'>Fehler beim Löschen der Datei.</span><br>";
      }
    }
    $i++;
  }
  flush();
}

function backupSummary($backupFiles) {
  global $allTime;
  global $mailText;
  $backupTime = time() - $allTime;
  if(count($backupFiles)>0) {
    echo "<br><span class='ok'>Die automatische Sicherung hat ".count($backupFiles)." Datensätze (Verzeichnisse/Datenbanken) in insgesamt $backupTime Sekunden gesichert.</span><br><br>";
    $mailText += "<br><span class='ok'>Die automatische Sicherung hat ".count($backupFiles)." Datensätze (Verzeichnisse/Datenbanken) in insgesamt $backupTime Sekunden gesichert.</span><br><br>";
  } else {
    echo "<br><span class='err'>Es scheint leider so als wenn keine Backups erfolgreich erstellt wurden.</span><br><br>";
    $mailText += "<br><span class='err'>Es scheint leider so als wenn keine Backups erfolgreich erstellt wurden.</span><br><br>";
  }
}

// function to get ftp connection object from URI
// basics were from: http://php.net/manual/de/function.ftp-connect.php#89811
function getFtpConnectionByURI($uri)
{
  // Split FTP URI into:
  // $match[0] = ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups
  // $match[1] = ftp
  // $match[2] = admin
  // $match[3] = password
  // $match[4] = barketing.dns.com
  // $match[5] = 21
  // $match[6] = /Data/FTP-Backups
  preg_match("/([a-z]*?):\/\/(.*?):(.*?)@(.*?):(.*?)(\/.*)/i", $uri, $match);
  
  $ftpCon = null;

  // check if port is set and uri is formatted correctly
  if(is_int(intval($match[5]))) {
    $port = intval($match[5]);

    // check if ftp(s) or sftp is chosen
    if($match[1]=="ftp") {
      // set up and ftp(s) connection, login
      echo "Stelle (FTPs über SSL - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
      // try ftps over ssl, usally through  port 21
      $ftpCon = ftp_ssl_connect($match[4], $port, 30);
      if (!gettype($ftpCon)=="resource") {
        echo "<span class='warn'>FTP über SSL - Port $port - Verbindung fehlgeschlagen!</span><br>";
        echo "Stelle (unsicheres FTP - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
        // try normal insecure ftp
        $ftpCon = ftp_connect($match[4], $port, 30);
      }
      if (gettype($ftpCon)=="resource") {
        $login = ftp_login($ftpCon, $match[2], $match[3]);
        $pasv = ftp_pasv($ftpCon, true);
      }
    } else if($match[1]=="sftp") {
      echo "<span class='err'>SFTP Unterstützung noch nicht implementiert.</span><br>";
      // if(!$port==22) {
      //    echo "<span class='warn'>SFTP Übertragung aber Port ist nicht 22. Ist der gewählte Port $port korrekt?</span><br>";
      // }
      // echo "Stelle (sichere sFTP - Port $port) Verbindung zu FTP Server $match[4]$match[5]:$match[6] her...<br>";
      // $ftpCon = ssh2_connect($match[4], $port, 30);
      // ssh2_auth_password($ftpCon, $match[2], $match[3]);
      // $sftp = ssh2_sftp($ftpCon);
    } else {
      echo "<span class='err'>Kein gültiger Verbindungstyp (ftp/sftp) angegeben.</span><br>";
    }
  } else {
    echo "<span class='err'>Der Port ist fehlerhaft angegeben. Bitte URI prüfen.</span><br>";
  }
  
  if ($ftpCon && gettype($ftpCon)=="resource")
  {
    echo "<span class='ok'>Verbindung hergestellt</span>";
    if ($login) {
      echo "<span class='ok'>, Login erfolgreich</span>";
      if ($pasv) {
        echo "<span class='ok'>, passiver Modus aktiviert</span>";
        if(!isset($match[6]) || $match[6] == "") {
          echo ".<br>";
          return $ftpCon;
        } else if (ftp_chdir($ftpCon, $match[6])) {
          echo "<span class='ok'>, Verzeichniswechsel zu $match[6] erfolgreich.</span><br>";
          return $ftpCon;
        } else {
          echo "<span class='err'>, Verzeichniswechsel zu $match[6] fehlerhaft.</span><br>";
          return null;
        }
      } else {
        echo "<span class='err'>, passiver Modus konnte nicht aktiviert werden. Upload wird trotzdem probiert.</span><br>";
        return $ftpCon;
      }
    } else {
      echo "<span class='err'>, Login fehlgeschlagen.</span><br>";
      return null;
    }
  }
  echo "<span class='err'>Fehler beim Verbinden mit dem FTP Server $match[4]$match[5]$match[6].</span><br>";
  echo "<p class='grey'>Debug:<br>";
  echo "URI (komplett): $match[0]<br>";
  echo "Typ: $match[1]<br>";
  echo "URI ohne Typ: $match[4]<br>";
  echo "Username: $match[2]<br>";
  echo "Passwort: $match[3]<br>";
  echo "Port: $match[5]<br>";
  echo "Unterordner: $match[6]<br>";
  echo "</p>";
  // Or retun null
  return null;
}

function copyBackupsToFtp($backupFiles, $backupType, $ftpCon) {
  if(empty($backupFiles)) {
    "<br><span class='err'>Es scheint leider so als wenn keine $backupType Backups erfolgreich erstellt wurden.</span><br><br>";
  } else {
    echo "<strong>".count($backupFiles)." $backupType Backups werden auf externen FTP kopiert...</strong><br>";
    if(gettype($ftpCon)=="resource") {
      foreach ($backupFiles as $fileName => $fileSize) {
        $uploadTime = time();
        echo "Kopiere $fileName (Größe: $fileSize MB) auf den FTP...<br>";
        flush();
        if (ftp_put($ftpCon, $fileName, $fileName, FTP_ASCII)) {
          $uploadEndTime = time() - $uploadTime;
          if (is_int($uploadEndTime)) {
            echo "<span class='ok'>Backup erfolgreich kopiert. (Dauer: $uploadEndTime Sekunden)</span><br>";
          } else {
            echo "<span class='ok'>Backup erfolgreich kopiert.</span><br>";
          }
        } else {
          echo "<span class='err'>Fehler beim Kopieren des Backups.</span><br>";
          continue;
        }
        flush();
      }
    }
  }
}

// from: http://stackoverflow.com/a/10473026/516047
function startsWith($haystack, $needle) {
  // search backwards starting from haystack length characters from the end
  return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}
?>

Sicherheit: Absicherung mit .htpasswd

Das Verzeichnis, in dem die backup.php und die Backups liegen, sollte natürlich mit einer .htpasswd abgesichert werden. Mit einer eingerichteten .htpasswd Datei ist zuerst ein Login nötig, eh man auf bestimmte Bereiche des Webspaces zugreifen darf:
Das Bild zeigt eine Login Datenabfrage beim Aufruf der Backup URL
Die Datei .htpasswd enthält hierbei die Login Daten und in der .htaccess des Backup Unterordners wird festgelegt, dass eine .htpasswd diesen Ordner schützt. Die .htpasswd generiert ihr euch am besten mit diesem Generator und baut sie dann folgendermaßen in die .htaccess dieses Ordners ein:

AuthType Basic
AuthName "Backups"
AuthUserFile /www/htdocs/all-inkl-account/backup/.htpasswd
Require valid-user

Sicherheit: Absicherung mit .htaccess

Da wir schonmal bei .htaccess sind, erhöhen wir die Sicherheit mit ein paar weiteren grundlegenden Zeilen:

#block access to certain file types
<FilesMatch ".(htaccess|htpasswd|ini|phps|log|sh|tar.gz)$">
 Order Allow,Deny
 Deny from all
</FilesMatch>

# disable directory browsing
Options All -Indexes

# prevent basic url hacking stuff
# from: http://www.queness.com/post/5421/17-useful-htaccess-tricks-and-tips
RewriteEngine On
# proc/self/environ? no way!
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
# Block out any script trying to set a mosConfig value through the URL
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|\%3D) [OR]
# Block out any script trying to base64_encode crap to send via URL
RewriteCond %{QUERY_STRING} base64_encode.*(.*) [OR]
# Block out any script that includes a <script> tag in URL
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
# Block out any script trying to set a PHP GLOBALS variable via URL
RewriteCond %{QUERY_STRING} GLOBALS(=|[|\%[0-9A-Z]{0,2}) [OR]
# Block out any script trying to modify a _REQUEST variable via URL
RewriteCond %{QUERY_STRING} _REQUEST(=|[|\%[0-9A-Z]{0,2})
# Send all blocked request to homepage with 403 Forbidden error!
RewriteRule ^(.*)$ /index.htm [F,L]

ErrorDocument 401 /backup/index.htm
ErrorDocument 403 /backup/index.htm
ErrorDocument 404 /backup/index.htm
ErrorDocument 500 /backup/index.htm

Dadurch werden Zugriffe auf bestimmte Dateitypen (auch die Backup Dateien), Verzeichnisse und Zugriffe mit sicherheitskritischen Merkmalen unterbunden. Alle diese nicht validen Zugriffe bekommen die index.htm serviert, welches einfach nur eine leere HTML Datei ist. Somit wird den Abfragenden auch kein detaillierter Grund gegeben, warum der Zugriff fehlschlug.

Automatisierung mit All-Inkl Cronjobs

Zu guter Letzt hilft diese Sicherungslösung natürlich nur, wenn sie automatisiert wird. Auch dies ist stark abhängig von eurem Hoster, System, dem Anwendungsbereich usw.
Im Falle von All-Inkl als Webhoster, könnt ihr die Cronjob Funktionalität im KAS (KAS -> Tools -> Cronjobs) benutzen:
Das Bild zeigt die Cronjob Einrichtungsoberfläche von All-Inkl


FTP-Backup-Lösung mit PHP

Dieser Beitrag ist eine Ergänzung bzw. Erweiterung des vorherigen Posts „FTP Backup Skript in PHP“.

Folgende Änderungen werde ich hier besprechen:

  • Erweiterung des Skripts
  • Sicherheit: Absicherung mit .htpasswd
  • Sicherheit: Absicherung mit .htaccess
  • Automatisierung mit All-Inkl Cronjobs

Erweiterung des Backup Skripts

Das neue Skript bietet nun neue Funktionalitäten:

  • Farbliche Hervorhebung
  • Verbesserungen des Backup Prozesses, zusätzliche Überprüfungen und Debug Infos bei Fehlern
  • Verbinden eines externen FTP Server und Kopieren aller neuen Backups
  • Angabe eines beliebigen Ports
  • Verbindung über FTPs (SSL FTP) Port 21 wird verwendet, unsicheres FTP nur noch als Fallback
  • detailliertere Informationen über die Backups in der Benachrichtigungsmail

Nun werden also von beliebig vielen Ordner des FTP-Root Backups erstellt, gegebenenfalls aufgeräumt wenn mehr Backups existieren als aufgehoben werden sollen und anschließend alle neuen Backups auf einen externen FTP Server kopiert. Für den Upload wird der passive FTP Modus verwendet, da dieser in den seltensten Fällen Probleme macht. Sollte der Wechsel zum passiven FTP fehlschlagen, wird dennoch aktives FTP probiert.
Alle nötigen Informationen werden in den Variablen in Zeile 16 bis 25 angegeben. Format und Hilfe steht jeweils dabei, eigentlich sollte da alles klar sein.

Die Erweiterungen machen aus dem Skript eine beispielhafte Backup-Lösung für FTP Inhalte. Auch hier am Beispiel von All-Inkl als Hoster. Wer die Lösung unabhängig von All-Inkl einsetzen möchte, wird das Archive_Tar PHP Modul und irgendeine Art von Cronjob-Funktionalität brauchen.

Screenshot

php-ftp-backup-neu

Code

Achtung: Update (09.06.2015): Dieses Skript ist Version 1.1, basierend auf dem grundlegenden Backup Skript aus diesem Artikel. In Version 1.2 (diesen Artikel) lassen sich nun auch MySQL Datenbanken mitsichern.

Update (02.06.2015): Anpassung der Variablennamen zur besseren Lesbarkeit, Fehlerkorrekturen, verbesserte FTP-URI-Verarbeitung, Portangabe möglich, Verbindung über FTPs (FTP über SSL über Port 21) möglich (mit Fallback zu unsicherem FTP wenn FTPs nicht funktioniert), erweiterte Informationen über die erfolgten Backups in der Benachrichtigungsmail, kleine Optimierungen
Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

<?
// PHP-Konfiguration optimieren
// if no errors are shown, please check htaccess restrictions by "php_flag display_errors off"
// in this or parent folders
@error_reporting(E_ALL);
@ini_set("max_execution_time", 300);
@ini_set("memory_limit", "256M");
header('Content-Type: text/html; charset=utf-8');
include "Archive/Tar.php";
$pfad = preg_replace('/(\/www\/htdocs\/\w+\/).*/', '$1', realpath(__FILE__));
$alltime = 0;
$newbackups = array();
$backupinfo = array();
echo "<style>.ok{color:#478F47;}.err{color:#DA3833;}.grey{color:grey;}.warn{color:#f0ad4e;}th,td{border-bottom: 1px solid #aaa;}</style>";

// ########## EDIT THIS VARIABLES ###################
$foldertobackup = array("tools", "reports"); // which root folders should get backed up?
$copytoexternalftp = 1; // copy new backup files to external ftp server? should be 1/"yes"/"ja" or 0/"no"/"nein"
// external (ftp) servers to copy new backups to, format:
// in general: ftp://username:password@url:port/path (port is required!)
// ftp://user:pw@ftp.server.com:21/
// ftp://user:pw@serverurl.com:21/optional/path
// secure sftp connection (port 22) in preparation but not ready yet
$externalftpuri = "ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups";
$backupfilemaximum = 2; // how many archives should be stored?
$dir = $pfad."backup/"; // in which subfolder is this backup php file? this would be: "root/backup/"
$sendmail = 1; // send notification mail when all backups are done - should be 1/"yes"/"ja" or 0/"no"/"nein"
$sendmailto = "adminmail@barketing.de"; // valid mail address to send the mail to
// ##################################################

echo "<span class='ok'>".date("d.m.Y G:i:s")."</span><br>";

foreach ($foldertobackup as $verzeichnis) {
   $jobtime = time();
   echo "<br><br>########################################<br>";
   echo "<strong>Verzeichnis $verzeichnis wird gesichert...</strong><br>";
   flush();

   // check if folder exists
   if(!file_exists($pfad.$verzeichnis)) {
      echo "<span class='err'>Sicherung fehlgeschlagen. Zu sichernder Ordner $pfad$verzeichnis existiert nicht.</span>";
      continue;
   }

   // Name: [verzeichnis]_[Datum]_[Uhrzeit].tar.gz
   $archivname = $verzeichnis.date('_Y-m-d_His').".tar.gz";
   // Name: [All-Inkl-Accountname]_[Datum]_[Uhrzeit].tar.gz
   //$archivname = preg_replace('/.+\/(.+)\/$/', '$1', $pfad).date('_Y-m-d_His').".tar.gz";

   // Auszuschließende Ressourcen
   $ignorieren = array("*.sql.gz", "*.tar.gz", "usage", "logs");

   // ######### create backup
   $archiv = new Archive_Tar($archivname, true);
   $archiv->setIgnoreList($ignorieren);
   $archiv->createModify($pfad.$verzeichnis, "", $pfad);
   $backuptime = time() - $jobtime;
   $archivsize = round(filesize($dir.$archivname)/1000000);
   if (!is_numeric($archivsize)) {
      $archivsize = "filesize error";
   }
   if (is_int($backuptime)) {
      echo "<span class='ok'>Backup fertig: $archivname (Größe: $archivsize MB, Dauer: $backuptime Sekunden)</span><br>";
   } else {
      echo "<span class='ok'>Backup fertig: $archivname</span><br>";
   }

   // check created archive
   if (!is_object($archiv)==1 || !is_numeric($archivsize) || !$archivsize>50) {
      // abort process due to wrong type or too small filesize (which likely is an error)
      echo "<span class='err'>Sicherung fehlgeschlagen. Erstelltes Archiv ist fehlerhaft.</span><br>Mehr Infos <a href='http://hannes-schurig.de/09/05/2015/ftp-backup-skript-in-php/' target='blank'>hier</a><br>";
      // debug
      echo "<p class='grey'>Debug:<br>";
      echo "Pfad: $dir.$archivname<br>";
      echo "Typ: ".gettype($archiv)."<br>";
      echo "Größe: $archivsize MB</p>";
      continue;
   }
   $newbackups[$archivname] = $dir.$archivname;

   echo "Aufräumen der Backups...<br>";
   flush();
   // integer starts at 0 before counting
   $i = 0;
   $backupfiles = array();
   // ######### collect valid backup files
   if ($handle = opendir($dir)) {
      while (($file = readdir($handle)) !== false) {
         if (  is_int(strpos($file, $verzeichnis)) == true &&
                  preg_match('/\.tar.gz$/i', $file) &&
                  !in_array($file, array('.', '..')) &&
                  !is_dir($dir.$file)
         ) {
            $backupfiles[$dir.$file] = filectime($dir.$file);
         }
      }
   }
   echo count($backupfiles)." valide Backups dieses Ordners gefunden, ";
   echo "$backupfilemaximum Backups sollen behalten werden. ";
   $backupcountdif = count($backupfiles)-$backupfilemaximum;
   if ($backupcountdif<=0) {
      echo "Kein Backup wird gelöscht.<br>";
   } else if ($backupcountdif==1) {
      echo "1 Backup wird gelöscht:<br>";
   } else if ($backupcountdif>=2) {
      echo "$backupcountdif Backups werden gelöscht:<br>";
   }
   flush();

   // ######### sort and delete oldest backups
   // sort backup files by date
   arsort($backupfiles);
   // reset counter variable
   $i = 0;
   // delete oldest files
   foreach ($backupfiles as $filepath => $value) {
      if($i>=$backupfilemaximum) {
         echo "$filepath wird gelöscht...<br>";
         if (unlink($filepath)) {
            echo "<span class='ok'>Datei erfolgreich gelöscht.</span><br>";
         } else {
            echo "<span class='err'>Fehler beim Löschen der Datei.</span><br>";
         }
      }
      $i++;
   }
   $jobendtime = time() - $jobtime;
   array_push($backupinfo, array($verzeichnis, $archivname, $archivsize." MB", $jobendtime." Sekunden", date("d.m.Y G:i:s")));
   echo "<span class='ok'>Backup für Verzeichnis $verzeichnis abgeschlossen.</span><br>";
   if (is_int($jobendtime)) {
      echo "######################################## (Dauer: $jobendtime Sekunden)<br>";
      $alltime += $jobendtime;
   } else {
      echo "########################################<br>";
   }
}

if(count($newbackups)>0) {
   echo "<br><span class='ok'>Die automatische Sicherung des Skripts '".pathinfo(__FILE__, PATHINFO_BASENAME)."' hat ".count($newbackups)." Verzeichnisse in insgesamt $alltime Sekunden gesichert.</span><br><br>";
} else {
   echo "<br><span class='err'>Es scheint leider so als wenn keine Backups erfolgreich erstellt wurden.</span><br><br>";
}
flush();

// ######### copy backups to external storages
if (!isset($copytoexternalftp) || $copytoexternalftp== 0 && in_array($copytoexternalftp, array("no", "nein"))) {
   echo "<span class='warn'>Backups werden nicht auf einen externen FTP kopiert. Option ist deaktiviert.</span><br>";
} else {
   $ftptime = time();
   echo "########################################<br>";
   echo "<strong>".count($newbackups)." Backups werden auf externen FTP kopiert...</strong><br>";
   flush();
   $ftpcon = getFtpConnectionByURI($externalftpuri);
   flush();
   if(gettype($ftpcon)=="resource") {
      foreach ($newbackups as $filename => $fullpath) {
         $uploadtime = time();
         echo "Kopiere .$filename. (Größe: $archivsize MB) auf den FTP...<br>";
         flush();
         if (ftp_put($ftpcon, $filename, $filename, FTP_ASCII)) {
            $uploadendtime = time() - $uploadtime;
            if (is_int($uploadendtime)) {
               echo "<span class='ok'>Backup erfolgreich kopiert. (Dauer: $uploadendtime Sekunden)</span><br>";
            } else {
               echo "<span class='ok'>Backup erfolgreich kopiert.</span><br>";
            }
         } else {
            echo "<span class='err'>Fehler beim Kopieren des Backups.</span><br>";
         }
         flush();
      }
   }
   $ftpendtime = time() - $ftptime;
   if (is_int($ftptime)) {
      echo "######################################## (Dauer: $ftpendtime Sekunden)<br>";
   } else {
      echo "########################################<br>";
   }
}
flush();

// ######### send mail
if (!isset($sendmail) || $sendmail== 0 && in_array($sendmail, array("no", "nein"))) {
   echo "<br><span class='warn'>Benachrichtigungsmail wurde nicht verschickt. Option ist deaktiviert.</span><br>";
} else {
   if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $sendmailto)) {
      echo "<br><span class='err'>FEHLER: Mail konnte nicht versendet werden, da die Adresse ungültig ist!</span><br>";
   } else {
      $mailsubject = "Automatische FTP Sicherung abgeschlossen";
      $mailtext = "Die automatische Sicherung des FTP-PHP-Backup-Skripts ".pathinfo(__FILE__, PATHINFO_BASENAME)." hat ".count($foldertobackup)." Verzeichnisse in insgesamt $alltime Sekunden gesichert.<br><br>";
      
      // add backup informations as table to mailtext
      $mailtext .= "<table border=0 style='border-spacing:0;'><thead><tr><th>Verzeichnis</th><th>Dateiname</th><th>Größe</th><th>Dauer</th><th>Timestamp</th></tr></thead><tbody>";
      for($i=0;$i<count($backupinfo);$i++) {
         $mailtext .= "<tr>";
         for($j=0;$j<count($backupinfo[$i]);$j++) {
            $mailtext .= "<td>" . $backupinfo[$i][$j] . "</td>";
         } 
         $mailtext .="</tr>";
      }
      $mailtext .= "</tbody></table>";
      mail(
         $sendmailto,
         $mailsubject,
         $mailtext,
         "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
      ) or die("<br><span class='err'>FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden.</span><br>");
      echo "<br><span class='ok'>Benachrichtigungsmail wurde erfolgreich verschickt!</span><br>";
   }
}
flush();

// function to get ftp connection object from URI
// basics were from: http://php.net/manual/de/function.ftp-connect.php#89811
function getFtpConnectionByURI($uri)
{
   // Split FTP URI into:
   // $match[0] = ftp://admin:password@barketing.dns.com:21/Data/FTP-Backups
   // $match[1] = ftp
   // $match[2] = admin
   // $match[3] = password
   // $match[4] = barketing.dns.com
   // $match[5] = 21
   // $match[6] = /Data/FTP-Backups
   preg_match("/([a-z]*?):\/\/(.*?):(.*?)@(.*?):(.*?)(\/.*)/i", $uri, $match);
   
   $ftpcon = null;

   // check if port is set and uri is formatted correctly
   if(is_int(intval($match[5]))) {
      $port = intval($match[5]);

      // check if ftp(s) or sftp is chosen
      if($match[1]=="ftp") {
         // set up and ftp(s) connection, login
         echo "Stelle (FTPs über SSL - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
         // try ftps over ssl, usally through  port 21
         $ftpcon = ftp_ssl_connect($match[4], $port, 30);
         if (!gettype($ftpcon)=="resource") {
            echo "<span class='warn'>FTP über SSL - Port $port - Verbindung fehlgeschlagen!</span><br>";
            echo "Stelle (unsicheres FTP - Port $port) Verbindung zu FTP Server $match[4] her...<br>";
            // try normal insecure ftp
            $ftpcon = ftp_connect($match[4], $port, 30);
         }
         if (gettype($ftpcon)=="resource") {
            $login = ftp_login($ftpcon, $match[2], $match[3]);
            $pasv = ftp_pasv($ftpcon, true);
         }
      } else if($match[1]=="sftp") {
         echo "<span class='err'>SFTP Unterstützung noch nicht implementiert.</span><br>";
         // if(!$port==22) {
         //    echo "<span class='warn'>SFTP Übertragung aber Port ist nicht 22. Ist der gewählte Port $port korrekt?</span><br>";
         // }
         // echo "Stelle (sichere sFTP - Port $port) Verbindung zu FTP Server $match[4]$match[5]:$match[6] her...<br>";
         // $ftpcon = ssh2_connect($match[4], $port, 30);
         // ssh2_auth_password($ftpcon, $match[2], $match[3]);
         // $sftp = ssh2_sftp($ftpcon);
      } else {
         echo "<span class='err'>Kein gültiger Verbindungstyp (ftp/sftp) angegeben.</span><br>";
      }
   } else {
      echo "<span class='err'>Der Port ist fehlerhaft angegeben. Bitte URI prüfen.</span><br>";
   }
   
   if ($ftpcon && gettype($ftpcon)=="resource")
   {
      echo "<span class='ok'>Verbindung hergestellt</span>";
      if ($login) {
         echo "<span class='ok'>, Login erfolgreich</span>";
         if ($pasv) {
            echo "<span class='ok'>, passiver Modus aktiviert</span>";
            if(!isset($match[6]) || $match[6] == "") {
               echo ".<br>";
               return $ftpcon;
            } else if (ftp_chdir($ftpcon, $match[6])) {
               echo "<span class='ok'>, Verzeichniswechsel zu $match[6] erfolgreich.</span><br>";
               return $ftpcon;
            } else {
               echo "<span class='err'>, Verzeichniswechsel zu $match[6] fehlerhaft.</span><br>";
               return null;
            }
         } else {
            echo "<span class='err'>, passiver Modus konnte nicht aktiviert werden. Upload wird trotzdem probiert.</span><br>";
            return $ftpcon;
         }
      } else {
         echo "<span class='err'>, Login fehlgeschlagen.</span><br>";
         return null;
      }
   }
   echo "<span class='err'>Fehler beim Verbinden mit dem FTP Server $match[4]$match[5]$match[6].</span><br>";
   echo "<p class='grey'>Debug:<br>";
   echo "URI (komplett): $match[0]<br>";
   echo "Typ: $match[0]<br>";
   echo "URI ohne Typ: $match[4]<br>";
   echo "Username: $match[2]<br>";
   echo "Passwort: $match[3]<br>";
   echo "Port: $match[5]<br>";
   echo "Unterordner: $match[6]<br>";
   echo "</p>";
   // Or retun null
   return null;
}
// from: http://stackoverflow.com/a/10473026/516047
function startsWith($haystack, $needle) {
   // search backwards starting from haystack length characters from the end
   return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE;
}

?>

Sicherheit: Absicherung mit .htpasswd

Das Verzeichnis, in dem die backup.php und die Backups liegen, sollte natürlich mit einer .htpasswd abgesichert werden. Mit einer eingerichteten .htpasswd Datei ist zuerst ein Login nötig, eh man auf bestimmte Bereiche des Webspaces zugreifen darf:
Das Bild zeigt eine Login Datenabfrage beim Aufruf der Backup URL
Die Datei .htpasswd enthält hierbei die Login Daten und in der .htaccess des Backup Unterordners wird festgelegt, dass eine .htpasswd diesen Ordner schützt. Die .htpasswd generiert ihr euch am besten mit diesem Generator und baut sie dann folgendermaßen in die .htaccess dieses Ordners ein:

AuthType Basic
AuthName &quot;Backups&quot;
AuthUserFile /www/htdocs/all-inkl-account/backup/.htpasswd
Require valid-user

Sicherheit: Absicherung mit .htaccess

Da wir schonmal bei .htaccess sind, erhöhen wir die Sicherheit mit ein paar weiteren grundlegenden Zeilen:

#block access to certain file types
&lt;FilesMatch &quot;.(htaccess|htpasswd|ini|phps|log|sh|tar.gz)$&quot;&gt;
 Order Allow,Deny
 Deny from all
&lt;/FilesMatch&gt;

# disable directory browsing
Options All -Indexes

# prevent basic url hacking stuff
# from: http://www.queness.com/post/5421/17-useful-htaccess-tricks-and-tips
RewriteEngine On
# proc/self/environ? no way!
RewriteCond %{QUERY_STRING} proc/self/environ [OR]
# Block out any script trying to set a mosConfig value through the URL
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|\%3D) [OR]
# Block out any script trying to base64_encode crap to send via URL
RewriteCond %{QUERY_STRING} base64_encode.*(.*) [OR]
# Block out any script that includes a &lt;script&gt; tag in URL
RewriteCond %{QUERY_STRING} (&lt;|%3C).*script.*(&gt;|%3E) [NC,OR]
# Block out any script trying to set a PHP GLOBALS variable via URL
RewriteCond %{QUERY_STRING} GLOBALS(=|[|\%[0-9A-Z]{0,2}) [OR]
# Block out any script trying to modify a _REQUEST variable via URL
RewriteCond %{QUERY_STRING} _REQUEST(=|[|\%[0-9A-Z]{0,2})
# Send all blocked request to homepage with 403 Forbidden error!
RewriteRule ^(.*)$ /index.htm [F,L]

ErrorDocument 401 /backup/index.htm
ErrorDocument 403 /backup/index.htm
ErrorDocument 404 /backup/index.htm
ErrorDocument 500 /backup/index.htm

Dadurch werden Zugriffe auf bestimmte Dateitypen (auch die Backup Dateien), Verzeichnisse und Zugriffe mit sicherheitskritischen Merkmalen unterbunden. Alle diese nicht validen Zugriffe bekommen die index.htm serviert, welches einfach nur eine leere HTML Datei ist. Somit wird den Abfragenden auch kein detaillierter Grund gegeben, warum der Zugriff fehlschlug.

Automatisierung mit All-Inkl Cronjobs

Zu guter Letzt hilft diese Sicherungslösung natürlich nur, wenn sie automatisiert wird. Auch dies ist stark abhängig von eurem Hoster, System, dem Anwendungsbereich usw.
Im Falle von All-Inkl als Webhoster, könnt ihr die Cronjob Funktionalität im KAS (KAS -> Tools -> Cronjobs) benutzen:
Das Bild zeigt die Cronjob Einrichtungsoberfläche von All-Inkl

All-Inkl bietet sogar das Zusenden aller Skriptausgaben. Ihr erhaltet also zusätzlich zu dem kurzen E-Mail-Bericht, der im Skript generiert werden kann, noch eine weitere Mail mit allen Ausgaben. Diese sind dann zwar nicht mehr farbig, aber was solls:
Das Bild zeigt die Cronjob E-Mail mit den Skriptausgaben

Danke an Kenny für das wertvolle Feedback im letzten Artikel, das dazu beigetragen hat, dass ich diese Erweiterung nochmal gepostet habe. Meinst du, dass das jetzt eine Basic Backup Lösung sein könnte? Das einzige, was noch fehlt, ist das Backup Management auf dem externen FTP Server, damit der nicht überläuft. Aber das ist nun wirklich Aufgabe der Admins zu entscheiden und zu verwalten, wie lange Backups aufgehoben werden sollen.


FTP-Backup-Skript in PHP

Achtung: Diese Backup Lösung habe ich im nächsten Artikel noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen und die darin enthaltenen Sicherheitshinweise zu beachten.

FTP Backups (bisher)

Mein Blog läuft auf dem Hoster ALL-INKL.com, den ich nebenbei gesagt nach mehreren Jahren immernoch empfehlen kann.
Bisher habe ich halbwegs (un)regelmäßig manuell, von Hand, FTP Backups erstellt. Also alle paar Monate habe ich über einen FTP Tool wie FileZilla bestimmte Ordner heruntergeladen, Datei für Datei, anschließend in ein Archiv gepackt und irgendwo verstaut. Dieser Prozess ist natürlich in vielerlei Hinsicht nicht zuverlässig, zeitintensiv und aufwändig.
Auch das Sichern der Ordner automatisiert mit Tools wie SyncBack (mein Artikel dazu) wird immer zeitintensiver, je größer die zu sichernde Datenmenge wird. Außerdem muss immer ein PC, auf dem die Tools laufen, an sein.

FTP Backups via PHP Skript

Sinnvoller ist es, diese Backups auf dem Server des Webhosters erstellen zu lassen. So wird der heimische PC nicht belastet.
All-Inkl bietet dafür ein recht einfaches PHP Skript zum Sichern eines einzelnen FTP Ordners oder des gesamten All-Inkl FTP Accounts.

Ich habe dieses Skript ausgebaut und um verschiedene Funktionen ergänzt:

  • beliebig viele Ordner des All-Inkl Accounts in einzelne .tar.gz Archive sichern
  • Einschränkung der Anzahl aufgehobener Backups – älteste Backups werden automatisch gelöscht
  • detaillierte Ausgabe inklusive benötigter Zeit
  • E-Mail Benachrichtigung

Screenshot

Das Bild zeigt die Ausgaben des Backup Skripts und die versendete E-Mail Benachrichtigung

Code

Achtung: Dieses Backup Skript habe ich in Version 1.1 noch einmal stark erweitert. Ich empfehle diese komplexere Lösung zu benutzen. Besser wäre sogar die Version 1.2, mit der sich zusätzlich MySQL Datenbanken mitsichern lassen.

Code anzeigenDen Code könnt ihr bequem mit den Links/Rechts Pfeiltasten horizontal bewegen.

<?
  // PHP-Konfiguration optimieren
  // if no errors are shown, please check htaccess restrictions by "php_flag display_errors off"
  // in this or parent folders
  @error_reporting(E_ALL);
  @ini_set("max_execution_time", 300);
  @ini_set("memory_limit", "256M");
  header('Content-Type: text/html; charset=utf-8');
  include "Archive/Tar.php";
  $pfad = preg_replace('/(\/www\/htdocs\/\w+\/).*/', '$1', realpath(__FILE__));
  $alltime = 0;

  // ########## EDIT THIS VARIABLES ###################
  $foldertobackup = array("bonnie", "tests", "locationmap", "blog"); // which root folders should get backed up?
  $backupfilemaximum = 2; // how many archives should be stored?
  $dir = $pfad."backup/"; // in which subfolder is this backup php file? this would be: "root/backup/"
  $sendmail = 1; // send notification mail when all backups are done - should be 1/"yes"/"ja" or 0/"no"/"nein"
  $sendmailto = "admin@yourdomain.com"; // valid mail address to send the mail to
  // ##################################################

  foreach ($foldertobackup as $verzeichnis) {
    $jobtime = time();
    echo "<br><br>########################################<br>";
    echo "<strong>Verzeichnis ".$verzeichnis." wird gesichert...</strong><br>";
    flush();

    // Name: [verzeichnis]_[Datum]_[Uhrzeit].tar.gz
    $archivname = $verzeichnis.date('_Y-m-d_His').".tar.gz";
    // Name: [All-Inkl-Accountname]_[Datum]_[Uhrzeit].tar.gz
    //$archivname = preg_replace('/.+\/(.+)\/$/', '$1', $pfad).date('_Y-m-d_His').".tar.gz";

    // Auszuschließende Ressourcen
    $ignorieren = array("*.sql.gz", "*.tar.gz", "usage", "logs");

    // ######### create backup
    $archiv = new Archive_Tar($archivname, true);
    $archiv->setIgnoreList($ignorieren);
    $archiv->createModify($pfad.$verzeichnis, "", $pfad);
    $backuptime = time() - $jobtime;
    if (is_int($backuptime)) {
      echo "Backup fertig: ".$archivname." (Dauer: ".$backuptime." Sekunden)<br>";
    } else {
      echo "Backup fertig: ".$archivname."<br>";
    }

    echo "Aufräumen der Backups...<br>";
    flush();
    // integer starts at 0 before counting
    $i = 0;
    $backupfiles = array();
    // ######### collect valid backup files
    if ($handle = opendir($dir)) {
      while (($file = readdir($handle)) !== false) {
        if (  is_int(strpos($file, $verzeichnis)) == true &&
              preg_match('/\.tar.gz$/i', $file) &&
              !in_array($file, array('.', '..')) &&
              !is_dir($dir.$file)
        ) {
          $backupfiles[$dir.$file] = filectime($dir.$file);
        }
      }
    }
    echo count($backupfiles)." valide Backups dieses Ordners gefunden, ";
    echo $backupfilemaximum." Backups sollen behalten werden. ";
    $backupcountdif = count($backupfiles)-$backupfilemaximum;
    if ($backupcountdif<=0) {
      echo "Kein Backup wird gelöscht.<br>";
    } else if ($backupcountdif==1) {
      echo "1 Backup wird gelöscht:<br>";
    } else if ($backupcountdif>=2) {
      echo $backupcountdif." Backups werden gelöscht:<br>";
    }
    flush();

    // ######### sort and delete oldest backups
    // sort backup files by date
    arsort($backupfiles);
    // reset counter variable
    $i = 0;
    // delete oldest files
    foreach ($backupfiles as $key => $value) {
      if($i>=$backupfilemaximum) {
        echo $key." wird gelöscht...<br>";
        if (unlink($key)) {
          echo "Datei erfolgreich gelöscht.<br>";
        } else {
          echo "Fehler beim Löschen der Datei.<br>";
        }
      }
      $i++;
    }
    $jobendtime = time() - $jobtime;
    if (is_int($jobendtime)) {
      echo "######################################## (Dauer: ".$jobendtime." Sekunden)<br>";
      $alltime += $jobendtime;
    } else {
      echo "########################################<br>";
    }
  }

  echo "<br><br>Die automatische Sicherung des FTP-PHP-Backup-Skripts '".pathinfo(__FILE__, PATHINFO_BASENAME)."' hat ".count($foldertobackup)." Verzeichnisse in insgesamt ".$alltime." Sekunden gesichert.<br><br>";

  // ######### send mail
  if (!isset($sendmail) || $sendmail== 0 && in_array($sendmail, array("no", "nein"))) {
    echo "Benachrichtigungsmail wurde nicht verschickt.";
  } else {
    if(!preg_match( '/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/' , $sendmailto)) {
      echo "FEHLER: Mail konnte nicht versendet werden, da die Adresse ungültig ist!";
    } else {
      mail(
        $sendmailto,
        "Automatische FTP Sicherung abgeschlossen",
        "Die automatische Sicherung des FTP-PHP-Backup-Skripts ".pathinfo(__FILE__, PATHINFO_BASENAME)." hat ".count($foldertobackup)." Verzeichnisse in insgesamt ".$alltime." Sekunden gesichert.",
        "From: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Reply-To: backupscript@{$_SERVER['SERVER_NAME']}\r\n" . "Content-Type: text/html\r\n"
      ) or die("FEHLER: Mail konnte wegen eines unbekannten Fehlers nicht versendet werden");
      echo "Benachrichtigungsmail wurde erfolgreich verschickt!";
    }
  }
?>

Wichtige Anmerkungen

Bitte beachtet, dass einige Zeilen angepasst werden müssen und der Code nur bei dem Webhoster All-Inkl getestet wurde. Die Zeile include "Archive/Tar.php", die ein externes Modul einbindet, könnte auf anderen Websern zu Problemen führen. Am besten testet ihr es einfach.
Thema Sicherheit: Wie Kenny in den Kommentaren korrekt angemerkt hat, müssen noch Anpassungen erfolgen, wenn das Skript als tatsächliche Backup-Lösung zum Einsatz kommen soll.

  • Die Sicherungen landen bei diesem Skript direkt in dem Skript-Ordner. Die Sicherung(en) sollte(n) nach der Sicherung an einen anderen Ort, beispielsweise auf einen anderen Server oder ein NAS, kopiert werden. Oder ihr automatisiert den Download der Sicherungen nach dem Backupprozess.
  • Sowohl das Skript als auch die Sicherungen können ohne weitere Maßnahmen direkt angesprochen werden. Ihr solltet natürlich den Backup Ordner beispielsweise durch eine htpasswd absichern und den Zugriff auf die Backups über die htaccess einschränken.

Ich werde dieses Online-Backup Thema in Zukunft vermutlich nochmal aufgreifen, erweitern und optimieren. Ich verlinke das dann hier.


Dieser Beitrag soll nur alle meine Batch-Softwaredeployments in einer Übersicht zusammenfassen:

Firefox

Stand: 19.09.2016 – Version: 48.0.2 getestet und läuft
Beitrag

Flash

Stand: 19.09.2016 – Version: 23.0 getestet und läuft
Beitrag

Java

Stand: 16.08.2016 – Version: 8u101 getestet und läuft
Beitrag

Neu: Reader DC

Stand: 16.08.2016 – Version DCUpd1501720053 getestet und läuft
Deployment-Guide

Reader (klassisch, Reader XI)

Stand: 28.07.2015 – Version: 11.0.12 getestet und läuft (deaktiviert)
Beitrag, Update-Kurzfassund 1 und 2

Skype

Stand: 17.03.2016 – Version: 7.21 getestet und läuft (deaktiviert)
Beitrag, Update-Kurzfassung

KeePass 2

Stand: 11.07.2016 – Version: 2.34 getestet und läuft
Beitrag

HipChat

Stand: 19.09.2016 – Version: 4.27.1.1658 getestet und läuft
Beitrag


Google scannt die meisten Webseiten täglich mehrmals. Wenn dabei Malware gefunden wird, wird diese Webseite auf eine Blacklist gesetzt, auf die alle größeren Browser zugreifen. So kann es schnell passieren, dass die eiene Seite bzw. eine bestimmte Unterseite so aussieht:
website-von-google-malware-warnung-befreien-warnung

Wenn nicht die komplette Webseite sondern nur eine Unterseite betroffen ist, sieht die detaillierte Google Malware Warnung vielleicht so aus:
website-von-google-malware-warnung-befreien-detaillierte-warnung

[…] 131 Seiten der Webseite überprüft. Dabei haben wir auf 1 Seite(n) festgestellt, dass Malware […]

Natürlich hat das Bereinigen der Seite, Entfernen von Schadcode und Schließen von Sicherheitslücken die oberste Priorität. Sollte das aber länger dauern oder die Unterseite muss am besten sofort wieder ohne Malware-Warnung erreichbar sein, muss eine schnelle Alternative her.

In diesem Fall ist es eventuell möglich die Malware Meldung mit einem Trick zu umgehen, ohne dass der Nutzer das merkt und ohne ihn zu gefährden.

How-To

1) Erstellt eine Kopie der Unterseite, also eine neue Seite mit exakt den gleichen Inhalten. Diese Seite sieht nun genauso aus wie die Malware-betroffene Seite, ist Malware-frei (wenn tatsächlich nur die eine Unterseite und nicht alle Seiten von der Malware betroffen war), allerdings unter einer anderen URL erreichbar.

2) Richtet eine Weierleitung der URL der beschädigten Seite zur URL der neuen Seite ein. Nutzt dazu entweder die Möglichkeiten eures CMS, eures DNS/Domain-Anbieters oder benutzt .htaccess Weiterleitungen:

Redirect 301 /about /z-portable

Dies würde die Kontaktseite meines Blogs direkt auf die Z-Portable Seite umleiten, mit dem HTTP Statuscode 301.

3) Nun wird der Besucher bei Eingabe der üblichen Malware-URL auf die Malware-freie Seite geleitet. Der Nutzer merkt das nur noch an der falschen URL, das verwirrt natürlich. Also sollte man noch die URL anpassen, damit für den Nutzer nicht mehr ersichtlich ist, dass er auf der Kopie-Seite befindet.
Am einfachsten geht das mit der Browser history API, um genau zu sein history.pushState:

// URL Fake
if(window.location.pathname=="/neue-url/") history.pushState({id: 'SOME ID'}, '', '/alte-url');

Damit wird, wenn im Browser die URL domain.tld/neu-url aufgerufn wird, die URL in domain.tld/alte-url in die URL Zeile gepackt. Nun ist nur noch mit Tools ersichtlich, dass eine andere URL aufgerufen wurde.

Beispiel

Die Unterseite meines Blogs /about/, also die Über Mich Seite, wurde von Malware infiziert; jedoch nur diese Unterseite.
Ich erstelle eine Kopie dieser Seite (exemplarisch nehme ich jetzt die Z-Portable Seite als „Kopie“) und leite die About Seite per htaccess darauf um. Nach einem Klick auf „About“ im Menü muss der Inhalt der Z-Portable Seite erscheinen, jedoch muss in der URL /about/ stehen, nicht /z-portable/.
website-von-google-malware-warnung-befreien-trick
Sichtbar ist ein anderer Inhalt, die URL passt.

Anschließen kann man über die Google Webmaster Tools die Neubewertung der Seite initiieren und sollte spätestens 12 Stunden später eigentlich diese Meldung erhalten:

Congratulations! Google has received and processed your malware review request. We did not detect any malware on your site.

Hier noch ein Guide zum Thema Google Malware Warnung: Link.


Batch – REM Kommentare besser als „::“

Ein Leser berichtete mir von seinen Erfahrungen mit Kommentaren in Batch:
REM ist gut, :: ist BÖSE!

Seine Erklärung dazu:

Grund: die ‚::‘-Kommentare sind syntaktisch anscheinend nichts anderes als Sprungmarken (die eben nie angesprungen werden). Funktioniert ja auch immer schön, und man kann sogar (vermeintlich) diese Sprungmarken doppelt definieren, also mehrfach denselben „Kommentar“ schreiben.
Probleme macht das aber in for-Schleifen. Konkret schicke ich dir mal ein Beispiel, bei dem die Meldung „Das System kann das angegebene Laufwerk nicht finden.“ erscheint.
Das wird noch viel lustiger, wenn man ein längeres Skript in dieser for-Schleife hat. Ich habe schon drei verschiedene sinnlose Fehlermeldungen wegen dieser Sache erhalten. War nicht leicht herauszufinden, was die Ursache war. Umstellen auf REM hat alle Probleme auf einmal behoben.

Hier der Start-Code für den Test:

@echo off
for /f "tokens=*" %%a in (test.bat) do (
    ::test
    ::echo
    echo %%a
)

Diesen Code habe ich Stück für Stück verändert und mir die Resultate angesehen. Und diese waren, wie vom Leser berichtet, erstaunlich fehlerhaft:
batch-kommentare-rem-statt-doppelpunkt-test-cmd
Ausführungen:

  1. Start-Code wie oben mit ::test und ::echo
  2. nur ::echo
  3. nur ::test
  4. ohne ::test und ::echo
  5. ::test an verschiedenen Stellen zw. echo %%a und der schließenden Klammer
  6. ::test außerhalb der for Schleife
  7. ::test2 innerhalb der for Schleife
  8. ::test2 in ::test umbenannt
  9. ::test und eine leere Zeile davor
  10. ::echo hinzugefügt, ::test und ::echo innerhalb, Start-Code
  11. ::echo wieder entfernt, nur ::test

Also bei :: Kommentaren innerhalb der for Schleife kommt es unter Umständen zu Fehlern. Mal triggert ein :: Kommentar („test“ in diesem Fall) einen Fehler, mal nicht. Zukünftig werde ich alles mit REM auskommentieren.


In früheren Posts habe ich bereits typische Probleme mit Batch und Umlauten, sowieso Umlauten und dem Copyrightsymbol behandelt. Die Artikel beschreiben aber nur die Darstellung von Umlauten und dem Copyrightsymbol. Probleme können jedoch auch bei der Verarbeitung von Daten mit Umlauten auftreten, beispielsweise beim Kopieren von Dateien mit Umlauten im Dateinamen.

2 Beispielcodes, die nicht funktionieren:

:: 1
xcopy stundenpläne.xlsx /test/

:: 2
for /f "tokens=*" %%a in (files.txt) do (
  xcopy "%~dp0%%a" "%~dp0test\%%a*" /Y
)

batch-umlaute-im-dateinamen-falsche-methode

Thomas, ein Leser, war so freundlich und schickte mir einen Code, der mit Umlauten in Dateinamen problemlos klarkommt.
Dieser Code nutzt ebenfalls Codepage Tricks um mit Umlauten in Dateinamen umgehen zu können:

:: Wechsele die Codepage, sonst kommen wir nicht im Umlauten in Dateinamen klar! (erst alte codepage speichern)
for /f "tokens=2 delims=:." %%a in ('chcp') do set alteCP=%%a
chcp 65001 >NUL

:: Finde alle referenzierten Dateien und lege sie in den Zielordner
for /f "tokens=*" %%a in (files.txt) do (
  :: Jetzt kopieren. Der Asterisk am Ende vermeidet, dass xcopy eine manuelle Eingabe erfordert.
  xcopy /Y "%~dp0%%a" "%~dp0test\%%a*"
)

:: Aufräumen.
chcp %alteCP% >NUL

Das einzig spezielle in dem Code ist der Stern im xcopy Befehl. Dieser verhindert, dass eine Eingabe vom Nutzer gefordert wird, wenn der Zielordner noch nicht existiert.


Admin

Sie möchten hier werben oder Sponsor auf längere Zeit werden? Ich garantiere Ihnen unschlagbare Preise bei meinen Besucherzahlen. Egal ob Banner, Textlinks oder andere Werbeformen. Informieren Sie sich bei mir unter werbung@hannes-schurig.de