blattertech informatikLukas BlatterNiederlenzerstrasse 21
5600 Lenzburg/AGSwitzerland

079 569 25 90
https://www.blattertech.ch

Adresse anzeigen

Sichere Downloads mittels .htaccess und FrontEndUsers (securefile1.1)

14. Juni 2011 |  .HTACCESS |  CMS MADE SIMPLE12 Kommentare

Um sichere Downloads (nur für authentifizierte Benutzer) zu ermöglichen gibt es verschiedene Wege. Ein Download-Manager-Modul wäre die eine Möglichkeit. Seit längerem gibt es für CMS Made Simple das Plugin securefile. Ich habe das Plugin nun ein wenig weiterentwickelt und den Einsatz vereinfacht.

Voraussetzung ist das FrontEndUser Modul welches den Zugriff auf die Dateien steuert.

Als erstes muss der Ordner der geschützt werden soll mit folgendem .htaccess ausgeschattet werden:

Options +FollowSymLinks
RewriteEngine on
RewriteBase /
RewriteRule ^(.+)$ download.php?fgroup=3&url=uploads/secfolder/$1 [L]

Dabei müssen folgende Parameter indivduell angepasst werden:

  • fgroup (zwingend): ID der FEU-Gruppe. Mehrere Gruppen können mit Komma getrennt werden
    Beispiel: fgroup=2 oder fgroup=2,3,8
  • url (zwingend): Pfad zum Ordner. Der Ordner muss in /uploads liegen.
    Beispiel: url=uploads/secfolder
  • pageid (optional): ID der Seite auf welche bei einem Fehler weitergeleitet werden soll
    Beispiel: page_id=95
  • pagealias (optional): Alias der Seite auf welche bei einem Fehler weitergeleitet werden soll.
    Beispiel: pagealias=fehlerseite

Zuletzt muss die download.php im CMSms Root (z.B. www.deineseite.com/download.php) abgelegt werden.
Hier die download.php:

<?php
# #####################################################################
#    coded for #CMS - CMS Made Simple
#    copyright (c) 2011 by BlatterTech Informatik (www.blattertech.ch)
#    Version 1.0 - 26.06.2007 inital release
#    Version 1.1 - 13.06.2011 modified by BlatterTech Informatik
#    Version 1.2 - 18.03.2012 modified by BlatterTech Informatik (Bugfixes, PHP5.3 ready)
# Version 1.3 - 08.08.2014 modified by BlatterTech Informatik (Bugfixes echo readfile --> $bytes = readfile)
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
# #####################################################################

require_once(dirname(__FILE__)."/include.php");

function redirect403 ($code = 403) {
    $gCms = cmsms();
    if (isset($_GET['pagealias']) and $_GET['pagealias'] != "" and $code != 404)  {
        $manager = $gCms->GetHierarchyManager();
        $node = $manager->sureGetNodeByAlias($_GET['pagealias']);
        if (isset($node)) {
            $content =& $node->GetContent();
            if ($content !== FALSE && is_object($content) && $content->Active() && $content->HasUsableLink()) {
                $url = $content->GetUrl();
                @session_start();
                $_SESSION['securedl'] = $_GET['url'];
                $_SESSION['securegid'] = $_GET['fgroup'];
                header("Location: ".$url);
                exit;
            }
        }
    }
    if (isset($_GET['pageid']) and $_GET['pageid'] != "" and $code != 404) {
        $manager = $gCms->GetHierarchyManager();
        $node = $manager->getNodeById($_GET['pageid']);
        if (isset($node)) {
            $content =& $node->GetContent();
            if ($content !== FALSE && is_object($content) && $content->Active() && $content->HasUsableLink()) {
                $url = $content->GetUrl();
                @session_start();
                $_SESSION['securedl'] = $_GET['url'];
                $_SESSION['securegid'] = $_GET['fgroup'];
                header("Location: ".$url);
                exit;
            }
        }
    }

    if ($code == 404) {
        header("HTTP/1.0 404 Not Found");
        echo "
            <html>
                <head>
                <title>404 Not Found</title>
                </head>
            <body>
                <p>File not found</p>
            </body>
            </html>
        ";
    }
    else {
        header("HTTP/1.0 403 Forbidden");
        echo "
            <html>
                <head>
                <title>403 Forbidden</title>
                </head>
            <body>
                <p>You have no access. Please login.</p>
            </body>
            </html>
        ";
    }
    exit;
}

// Control if all params are available
if (!isset($_GET['url']) or !isset($_GET['fgroup'])) {
    redirect403();
}

$pathinfo = pathinfo($_GET['url']);
$filename = $pathinfo['basename'];

//security check (folder must be in /uploads)
if (strpos($pathinfo['dirname'],"uploads/") === FALSE) {
    redirect403();
}

define("SECURE_FOLDER", dirname(__FILE__).DIRECTORY_SEPARATOR.$pathinfo['dirname']);
define("LOG_DOWNLOADS", true); //creates a log file in secure folder
define("DELIVER_CHUNKED", true); //enable if you have trouble with large files

############ do not edit below unless you know what you're doing ###########
@ini_set('zlib.output_compression', 'Off');

function readfile_chunked($filename,$retbytes=true) {
    $chunksize = 1*(1024*1024); //how many bytes per chunk
    $buffer = '';
    $cnt =0;
    $handle = fopen($filename, 'rb');
    if ($handle === false) {
        return false;
    }
    while (!feof($handle)) {
        $buffer = fread($handle, $chunksize);
        echo $buffer;
        ob_flush();
        flush();
        if ($retbytes) {
            $cnt += strlen($buffer);
        }
    }
    $status = fclose($handle);
    if ($retbytes && $status) {
           return $cnt; // return num. bytes delivered like readfile() does.
    }
    return $status;
}

function btHasAccess($feugroups) {
  if (!$feugroups) return false;
  $feusers = cms_utils::get_module('FrontEndUsers');
  $user_id = $feusers->LoggedInId();
  $groups = $feusers->GetMemberGroupsArray($user_id);

  $gns = array();
  if($groups !== false ) {
      foreach( $groups as $gid ) {
          $gns[] = $gid['groupid'];
      }
  }

  $feuArray= explode(',',$feugroups);
  $retval = false;
  foreach ($feuArray as $feu) {
      if (in_array($feu,$gns)) return true;
  }
  return false;
}

function btGetUsername() {
    $feusers = cms_utils::get_module('FrontEndUsers');
    return $feusers->LoggedInName();
}

// check FEU access
if (btHasAccess($_GET['fgroup'])) {
    $username = btGetUserName();
}
else {
    redirect403();
}

// check file_exists
if (!file_exists(SECURE_FOLDER.DIRECTORY_SEPARATOR.$filename)){
    redirect403(404);
}

$browser = $_SERVER['HTTP_USER_AGENT'];

if (strpos("Mac", $browser) !== false or strpos("MSIE", $browser) !== false) {
    $typ = "application/octet-stream";
}
else {
    $typ = "application/x-download";
}

//headers
header('Pragma: public');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate'); // HTTP/1.1
header('Cache-Control: pre-check=0, post-check=0, max-age=0'); // HTTP/1.1
header('Content-Transfer-Encoding: none');
header("Accept-Ranges: bytes");
header("Content-Type: $typ");
header("Content-Disposition: attachment; filename=\"{$filename}\";");

//deliver file
@set_time_limit(0); //make sure large files dont get timed out.
if (DELIVER_CHUNKED == true) {
    $bytes = @readfile_chunked(SECURE_FOLDER.DIRECTORY_SEPARATOR.$filename);
}
else {
    $bytes = @readfile(SECURE_FOLDER.DIRECTORY_SEPARATOR.$filename);
}
//log the download
if (LOG_DOWNLOADS == true) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $host = gethostbyaddr($ip);
    if (empty($host)) { $host = "UNRESOLVED"; }
    $datenow = date("Y.m.d");
    $timenow = date("H:i:s");

    $logfile = SECURE_FOLDER.DIRECTORY_SEPARATOR.".download_log.csv";
    $logdata = "";
    if (!is_file($logfile)) // add title at first line
        $logdata .="Date;Time;Filename;Username;IP-Adress;Host;Browser\n";
    $logdata .= $datenow.";".$timenow.";".$filename.";".$username.";".$ip.";".$host.";".$browser."\n";
    @file_put_contents($logfile, $logdata, FILE_APPEND | LOCK_EX);
}
exit;

 

Securefile1.3 herunterladen

Über den Autor:

Lukas Blatter

bloggt und twittert seit 2009 aus Leidenschaft diverse Themen rund um Webdesign, PHP, TYPO3, CMS Made Simple, Computer und weiteres.

Er ist Gründer und Inhaber von blattertech informatik, Ehemann und Vater einer Kinderschar.

Kommentare

2 kleine Korrekturen
18. März 2013 von Nuac15
Hallo,
im download.php haben sich 2 kleine Fehler eingeschlichen.
Zum einen wird die LOG_DOWNLOADS bei der Abfrage in Zeile 196 zu LOG_DOWNLOAD. Zum anderen wird in Zeile 170 der Browser abgefragt der erst in der if-Abfrage des logs definiert wird.

Beste Grüße und vielen Dank für die Bereitstellung des Scriptes.
Re: 2 kleine Korrekturen
18. März 2013 von Lukas Blatter
Danke für die Rückmeldung!

Ich habe das Script korrigiert und als Version 1.2 wieder verlinkt. Zusätzlich habe ich die ereg Funktion ersetzt.
Verstehe den Einsatz dieses Moduls nicht
1. August 2013 von Rafael Czernek
Hallo also ich habe alles gemacht wie beschrieben, aber wie gebe ich nun eine Datei in den href ein. Irgendwie geht dann nichts mehr!
Re: Verstehe den Einsatz dieses Moduls nicht
1. August 2013 von Lukas Blatter
Was hast du genau gemacht? Was für eine Fehlermeldung kommt?

Die Dateien welche sich im gesicherten Ordner befinden, können mit ganz normalen Links angesprochen werden. Das dieser Aufruf in der .htaccess Umgebogen wird, sieht man nirgends.

Hast du den Pfad zum geschützten Ordner in der .htaccess angepasst?
Klappt leider nicht?
6. November 2013 von Andi Müller
Hallo,
Habe das so gemacht wie beschrieben, kriege aber einen "Error 500". Die Beschreibung ist leider sehr knapp, vorallem in Bezug auf das FrontEndUser-Modul... Es gibt leider keine weiterführenden Informationen im Netz, schade...
Re: Klappt leider nicht?
21. November 2013 von Lukas Blatter
Ein Error 500 kommt wahrscheinlich vom .htaccess. Was hast du für einen Webserver? Welches Betriebssystem? Apache?

Gibt es eine Fehlermeldung wenn du das Downloadscript direkt aufrufst?

Welche weitern Infos brauchst du bezüglich FEU?
Mehr Info's bitte
29. Juli 2014 von Peer
Ich verwende aktuell noch securefile 1.0.
Dieses basiert auf einem Plugin für CMSMS und der download.php.
Kommt deine Version ohne Plugin aus, oder sollte das alte weiterverwendet werden. Wie sieht ein Beispieldownloadlink aus?

Danke
Re: Mehr Info's bitte
8. August 2014 von Lukas Blatter
Die Version kommt ohne Plugin aus. Der Zugriff auf die Datei wird einzig durch die .htaccess gesteuert. Du kannst ganz normal einen Link direkt auf die Datei machen. Beim Zugriff auf der Ordner wird jeder Zugriff via .htaccess auf die download.php umgebogen. In der download.php wird geprüft, ob der Zugriff erlaubt ist. Falls nein, wird eine Fehlermeldung angezeigt oder auf eine definierte Seite weitergeleitet.
Nicht mehr kompatibel mit CMSMS 2.1
1. Februar 2016 von Slurm
Nach dem Upgrade auf CMSMS 2.1 musste ich feststellen, dass dieses Script leider nicht mehr funktioniert. Als erstes wurde mal die include.php Datei in den /lib Ordner verschoben, aber auch mit einer Anpassung des Pfades im Script funktioniert dieses nicht. Ich werde ohne Fehlermeldung oder Download einfach auf meine Hauptseite geleitet.
RE: Nicht mehr kompatibel mit CMSMS 2.1
1. Februar 2016 von Slurm
Kurze Ergänzung: Den Pfad zur include.php muss man ändern. Danach wird auch alles richtig geladen. Auch das FrontEndUsers Modul wird geladen und steht bereit. Der Fehler ist, das jetzt keine Session gestartet ist. Dadurch schlägt die Abfrage des aktuellen Users fehl. In der Datei include.php sieht man auch bei Zeile 52, dass dort der Sessioncode auskommentiert ist. Vermutlich wird die Session bei CMSMS2 an einer anderen Stelle geladen. Wenn man diesen Teil des Codes wieder aktiv setzt funktioniert auch der Download wieder. Ich habe mir jetzt vorrübergehend eine Kopie der include Datei gemacht. include2.php. Diese ist identisch mit der originalen CMSMS 2.1 Datei, nur ist dort der Code aktiv gesetzt. Diese Datei importiere ich in der download.php . Die originale include-Datei lasse ich unberührt um weitere, unvorhersehbaren Fehler auf der Webseite auszuschließen.
RE: Nicht mehr kompatibel mit CMSMS 2.1
1. Februar 2016 von Lukas Blatter
Danke für die ausführliche Rückmeldung. Ich habe das Script bisher mit der 2er Version noch nicht getestet. Sobald ich eine Lösung gefunden habe, werde ich sie hier posten.
RE: Nicht mehr kompatibel mit CMSMS 2.1
23. März 2017 von Slurm
Ist zwar schon eine Weile her, aber der behelfsmäßige Workaround läuft bis heute in meinem Code. Kann ja eigentlich nicht sein, daher hab ich es mir nochmal angesehen und ich denke ich habe jetzt eine richtige Lösung. Und sie ist einfacher und offensichtlicher als gedacht. *facepalm*

require_once(dirname(__FILE__)."/lib/include.php");
setup_session();

Dadurch wird erstmal der Pfad zur include.php richtig gesetzt.
Diese liegt seit CMSMS 2.x im "/lib" Verzeichnis.
Die Session wird jetzt aber nicht mehr automatisch gestartet.
Darum der Aufruf zum Starten der Session.

Das war es schon. So läuft es wieder richtig.
Wenn man als FEU-User eingeloggt ist und die rechte hat kann man Dateien laden. Ist man nicht eingeloggt als FEU-User, erfolgt auch keinen Download. Funktioniert also weiterhin wie es soll mit der Anpassung.
RE: Nicht mehr kompatibel mit CMSMS 2.1
24. März 2017 von Lukas Blatter
Vielen Dank für den Hinweis.

Auf Grund des hohen Spamaufkommens können zu diesem Beitrag keine Kommentare mehr abgegeben werden