Geschütze Download-Url OHNE Timeout

Musterlösung

Grünschnabel
LÖSUNG: Geschütze Download-Url OHNE Timeout

so erstellt man einen geschützten Download-Link, der ohne PHP Timeout und bei jeder Dateigröße sauber läuft.

Das Prinzip sieht so aus:
Download-Link mit Timestamp wird auf Gültigkeit geprüft --> RemoteIP und Timestamp werden in DB erfasst --> .htaaccess mit IPs aus DB wird erstellt --> Download ist freigegeben

WICHTIG:
1. Man muss eine neue SQL Tabelle (Bezeichnung "IPListe") mit zwei Spalten ("ip", "timestamp") erstellen.
2. Auf dem Server einen neuen Ordner (Bezeichnung "sicher") erstellen. (Dort dürfen die Downloaddateien rein die geschützt werden sollen). Die Dateien "link.php" und "download.php" liegen einen Ordner weiter oben.

download.php (regelt den Download):

PHP:
<?PHP
//WICHTIG: neue sql datenbank manuell einrichten -- tabellenname: IPListe, spalte1: ip, spalte2: timestamp
$explode = explode('-', $_GET['code']);
 
$get_fileID = $explode[0];
$get_seal = $explode[1];
$get_timestamp = $explode[2];

$seal = substr(md5("blabla" . $get_fileID . $get_timestamp), 0, 3);
if($seal == $get_seal) {
    if(time() < $get_timestamp) {
		//sql zeugs
		$dbserver = "db.sqlservernamexy.de"; // MYSQL Hostname   <---- ANPASSEN************************************!
		$DB = "sqldb123"; // MYSQL Datenbankname   <---- ANPASSEN************************************!
		$dbuser = "sqluser123"; // MYSQL Username   <---- ANPASSEN************************************!
		$dbpass = "sqlpw123"; // MYSQL Passwort   <---- ANPASSEN************************************!
		$conn = mysql_connect($dbserver,$dbuser,$dbpass);
		$ip22 =  $_SERVER['REMOTE_ADDR'];
		$downloadzeit=10000; // Sekunden
		$DOCUMENT_ROOT = "/mnt/webe/test/sicher"; //   <---- ANPASSEN************************************!
		$TMPhta1 = "AuthType Basic" ."\n".  "order deny,allow" ."\n". "deny from all" ."\n" ;
		$faker403 = "ErrorDocument 403 '<html><head><title>404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>'";
		//mit db verbinden
		if (!$conn){
		die("Fehler! Die Datenbank ist nicht erreichbar!");
		}
		mysql_select_db($DB,$conn);
		// alle verfallenen Einträge löschen + doppelte ip löschen
		$query = "DELETE FROM IPListe WHERE (ip) = '".$ip22."'"; 
		$result = mysql_query($query) or die(mysql_error()); 
		$query = "DELETE FROM IPListe WHERE (timestamp) < '".time()."'"; 
		$result = mysql_query($query) or die(mysql_error()); 
		//ip einhacken
		$query = "INSERT INTO IPListe (ip, timestamp) VALUES('".$ip22."','" .(time()+$downloadzeit)."')"; 
		$result = mysql_query($query) or die(mysql_error()); 
		//db lesen
		$result = mysql_query("SELECT * From IPListe");
		while ($rows=mysql_fetch_array($result)) {
		$TMPips = $TMPips . "allow from " . $rows[ip] . "\n";
		}
		//htaccess schreiben
		$htaccess = fopen("$DOCUMENT_ROOT"."/.htaccess", "w");
		fputs($htaccess, $TMPhta1 . $TMPips . $faker403);
		fclose($htaccess);

//DOWNLOAD STARTEN
		if("dl1" == $get_fileID) {header("Location: http://www.meinedomain.de/test/sicher/datei1.zip");} //   <---- ANPASSEN************************************!
		if("dl2" == $get_fileID) {header("Location: http://www.meinedomain.de/test/sicher/datei1.zip");} //   <---- ANPASSEN************************************!
		if("dl3" == $get_fileID) {header("Location: http://www.meinedomain.de/test/sicher/datei1.zip");} //   <---- ANPASSEN************************************!
//ENDE

    } else {
        echo "Download ist nicht mehr gültig!";
    }
} else {
    echo "Fehler im Download-Link!";
}?>




link.php (erstellt Download-Links, Gültigkeit von 20 Tagen):


PHP:
<?PHP
$md5pw = "blabla";
$download_link1 =  "http://www.meinedomain.de/test/download.php?code=" .  "dl1" . "-" . md5($md5pw .  'dl1' . strtotime('+ 500 Hours')) . "-" . strtotime("+ 500 Hours"); //   <---- ANPASSEN************************************!
$download_link2 =  "http://www.meinedomain.de/test/download.php?code=" .  "dl2" . "-" . md5($md5pw .  'dl2' . strtotime('+ 500 Hours')) . "-" . strtotime("+ 500 Hours"); //   <---- ANPASSEN************************************!
$download_link3 =  "http://www.meinedomain.de/test/download.php?code=" .  "dl3" . "-" . md5($md5pw .  'dl3' . strtotime('+ 500 Hours')) . "-" . strtotime("+ 500 Hours"); //   <---- ANPASSEN************************************!

echo "Download 1" . "<br>";
echo $download_link1 . "<br>" . "<br>";
echo "Download 2" . "<br>";
echo $download_link2 . "<br>" . "<br>";
echo "Download 3" . "<br>";
echo $download_link3 . "<br>" . "<br>";
?>



Ein Download-Link sieht dann so aus:
http://www.meinedomain.de/test/download.php?code=dl1-d1a-0987145331
Link ist 20 Tage gültig und kann per Mail etc. verschickt werden.

PS: Absolute Musterlösung!
 
Öhm - ich seh jetzt grad nicht durch, wozu das gut sein soll.
Zudem schreibst du im Titel ohne Timout und nachher sind es 20 Tagen die der Link gültig bleibt.

Zudem solltes du dein $_GET['code'] unbedingt auf gültigkeit testen! filter_input() und Reguläre Ausdrücke sind da Stichworte.

Wozu machst du einen langen code-String und nicht gleich versch. GET-Parameter?
PHP:
$params['id'] = 'dl3';
$params['seal']l = md5($md5pw . $params['id'] . strtotime('+ 500 Hours'));
$params['ts'] = strtotime("+ 500 Hours");
$download_link3 =  'http://www.meinedomain.de/test/download.php?' . http_build_query($params);

Ehlrich gesagt, ich hab den Code jetzt 2 mal gelesen und verstehe nicht was da läuft. Mach doch mal noch ein Bisschen Erklärbär zu deiner Musterlösung.
 
Timeout bezieht sich auf das PHP Timeout Problem. Normalerweise werden geschützte Downloads per fread o.ä. via php übertragen und wenn die Downloaddatei zu groß ist bzw. der Download etwas länger dauert, dann tritt das PHP Timeout Problem auf und der Download wird abgebrochen. Timeout für ein PHP Script liegt so zwischen 60 und 120 Sekunden und wird vom Webhoster vorgegeben.
Die Lösung sieht halt dann so wie oben aus...
 
Musterlösung kann man das nicht nennen.
PHP:
$database = array(
  'username' => 'root',
  'password' => 'root',
  'hostname' => '127.0.0.1',
  'database' => 'root',
);
  
$download_time = 10000; # in seconds 

# database structure:
# table "ip_list":
# -- column "ip": VARCHAR(15)
# -- column "timestamp": TIMESTAMP
list($file_id, $seal, $timestamp) = explode('-', $_GET['code']);

try
{
  # if download seal is invalid
  if ($seal !== substr(md5('blabla' . $file_id . $timestamp), 0, 3))
  {
    throw new Exception('download link is invalid');
  }
  
  # if download link is expired
  if (time() >= $get_timestamp)
  {
    throw new Exception('download link is expired');
  }
  
  # build connection to the database host
  $connection = mysql_connect(
    $database['hostname'],
    $database['username'],
    $database['password']
  );
  
  # error while building connection to the database host
  if ($connection === false)
  {
    throw new Exception('could not build connection to the database');
  }
  
  # try to select database
  if (mysql_select_db($database['database'], $connection) === false)
  {
    throw new Exception('could not select database');
  } 
  
  # create sql query string
  $sql = sprintf(
    'DELETE FROM `ip_list` WHERE (`ip` = "%s") OR (`timestamp` < NOW())',
    $_SERVER['REMOTE_ADDR']
  );
  
  # send sql query to database and delete expired datasets
  if (mysql_query($sql, $connection) === false)
  {
    throw new Exception(mysql_error());
  }
  
  $sql = sprintf(
    'INSERT INTO `ip_list` (`ip`, `timestamp`) VALUES ("%s", %i)',
    $_SERVER['REMOTE_ADDR'],
    time() + $download_time
  );
  
  if (mysql_query($sql, $connection) === false)
  {
    throw new Exception(mysql_error());
  }
  
  if (($result = mysql_query('SELECT * FROM `ip_list`', $connection)) === false)
  {
    throw new Exception(mysql_error());
  }
  
  $addresses = array();
  
  while ($row = mysql_fetch_array($result))
  {
    $addresses[] = $row['ip'];
  }
  
  if (!in_array($_SERVER['REMOTE_ADDR'], $addresses))
  {
    header('HTTP/1.1 403');
    echo (
<<<HTML
<!DOCTYPE html>
<html>
<head>
  <title>403 Forbidden</title>
</head>
<body>
  <h1>403 Forbidden</h1>
</body>
</html>
HTML
    );
  }
  
  switch ($file_id)
  {
    case 'dl1':
      header('Location: http://www.meinedomain.de/test/sicher/datei1.zip');
      break;
    case 'dl2':
      header('Location: http://www.meinedomain.de/test/sicher/datei1.zip');
      break;
    case 'dl3':
      header('Location: http://www.meinedomain.de/test/sicher/datei1.zip');
      break;
  }
}
catch (Exception $e)
{
  die($e->getMessage());
}
Es ist verwerflich eine Datei wie .htaccess während der Laufzeit dynamisch zu ändern. Besser wäre es daher die IP-Adresse in PHP zu überprüfen.
 
Zuletzt bearbeitet:
@einfach nur crack

Sehr übersichtlich umprogrammiert.
Doch der Sinn soll ja sein, dass die Downloaddatei geschützt sein soll und nicht von jedem runtergeladen werden kann. Bei deinem Code verzichtet man ja auf die .htaaccess und somit ist der echte Downloadlink (ht tp://w ww.meinedomain.de/test/sicher/datei1.zip) für jeden zugänglich...
 
@einfach nur crack:
Wenn Du den Download ohne .htaccess machst, dann ist der DL nicht mehr wirklich geschützt.

@TE:
Aber als Musterlösung würde ich das auch nicht bezeichnen. Als Ansatz eventuell.

Gruß
 
Es geht mir nicht darum die .htaccess wegzulassen. Es geht mir darum, dass man nicht die Links darin dynamisch per PHP ändert. Natürlich braucht man .htaccess, wenn man die Dateien in einem für das Web zugängigen Verzeichnis speichert. Ansonsten einfach die Dateien in einem Verzeichnis außerhalb des Web-Verzeichnisses platzieren und schon kann da niemand mehr drauf zugreifen.
 
Auch mal meine 2 Cent:

- $_SERVER['REMOTE_ADDR'] ist ein potentieller Fehler, wenn man Proxies nicht außen vor lassen will. Vor allem User mit Zwangsproxy (z.B. AOL) könnten da ein false-positive provozieren.

- Keine oder so gut wie keine Fehlerbehandlung.

Musterlösung sieht wirklich anders aus. Vor allem, weil der eine oder andere Mitleser, der nicht so sehr erfahren ist, das ausprobieren und möglicherweise auf unvorhergesehene Probleme stoßen wird.

EDIT:
Mach doch mal noch ein Bisschen Erklärbär zu deiner Musterlösung.

Ein Schweizer kennt den Erklärbär? ;-)
 
Zuletzt bearbeitet:
@saftmeister
Welcher Befehl wäre statt $_SERVER['REMOTE_ADDR'] besser?

@alle
Zum Punkto Musterlösung: Das Prinzip mit der .htaccess ist genial. Die Umsetzung der Programmierung ist ein ganz anderes Thema - die könnte schon etwas schöner, besser, aufgeräumter, verständlicher usw. sein... Im Grunde wird die IP in die .htaccess geschrieben und fertig.

Es geht darum, dass in einer e-Mail ein zeitlich begrenzter Download-Link steht und man eine 200MB Datei auch übers klassische Modem problemlos herunterladen kann...

Solange hier niemand eine bessere Methode kennt, die unabhängig vom PHP Timeout funktioniert, die Downloaddatei vor öffentlichem Zugriff schützt und kein Login-Fenster o.ä. erstmal aufruft, trägt der Code die Bezeichnung "Musterlösung".
 
Genial ist das überhaupt nicht. Es birgt Gefahren:

- Nehmen wir mal an, es starten zwei Leute gleichzeitig den Download. Wie löst du die Race-Condition auf, bei der die .htaccess von User1 die .htaccess von User2 überschreibt? Was passiert eigentlich, wenn der Download aus welchen Gründen auch immer abgebrochen wird?

- Warum verwendest du statt einer .htaccess-Datei nicht http://www.php.net/manual/de/features.http-auth.php und deckst damit auch Webserver ab, die KEIN .htaccess unterstützen (z.B. IIS)?

- Um das $_SERVER['REMOTE_ADDR']-Problem bei Proxies zu lösen, könntest du zumindest mal nachschauen, ob HTTP_X_FORWARDED_FOR gesetzt ist und ggf. diese IP verwenden.

EDIT: einfach nur crack hat den richtigen Ansatz ja schon vorgezeigt. Natürlich geht es nicht ohne Verzeichnisschutz. Der sollte aber statisch bleiben. Gründe dafür hab ich ja schon genannt.
 
Zuletzt bearbeitet:
Zurück