Hans: geschützter PHP Download auf Windows Server

Hallo,

ich habe auf einem Windows 2012 Server in einem Verzeichnis das nicht über den IIS im Web über eine URL erreichbar ist diverse Zipfiles, die nun über ein Webadresse nach einem Login downloadbar gemacht werden müssen. Das Login läuft ganz normal über PHP und Sessions. Kann ich via PHP Dateien aus geschützen Verzeichnissen weiterreichen und so zum Download anbieten? Wäre hier Curl ein Ansatz?

Danke Hans

  1. Tach!

    Kann ich via PHP Dateien aus geschützen Verzeichnissen weiterreichen und so zum Download anbieten?

    Du kannst mit PHP über das Dateisystem auf alle Verzeichnisse und Dateien zugreifen, für die der Prozess eine Leseberechtigung hat. Das Prinzip ist dasselbe, wie wenn du als angemeldeter Nutzer auf Dateien zugreifen kannst, zum Beispiel beim Laden in einen Editor oder ein anderes Programm.

    Beim IIS ist es so, dass deine Anwendung in einer der "Sites" liegt und diese Site einem Application Pool zugeordnet ist. Der wiederum läuft unter einer bestimmten Identity und da ist ein User aus dem Windows-System konfiguriert. Der muss die Zugriffsrechte haben. Siehe Application Pool Identities.

    Wäre hier Curl ein Ansatz?

    Nein, damit kann man nicht aufs Dateisystem zugreifen sondern stellt Requests an andere Server, wie das jeder Browser macht.

    dedlfix.

    1. Danke für die Infos. Wäre dann file_get_contents eine Lösung? Z.B. so:

      $zipfile = 'C:\inetpub\db\daten\2017.zip';
      $filename = basename($zipfile);
      header('Content-Type: application/zip');
      header('Content-Disposition: attachment; filename=' . $filename);
      header('Content-Length: ' . filesize($zipfile));
      echo file_get_contents($zipfile); 
      

      Funktioniert das auch zuverlässig mit großen Datenpaketen? Gruß Hans

      1. Tach!

        Wäre dann file_get_contents eine Lösung?

        Ja, aber readfile() ist die bessere Alternative. file_get_contents() liest den gesamten Inhalt in den Speicher und echo gibt ihn aus. readfile() hingegen liest stückweise und gibt diese Stücken aus (braucht auch kein echo dafür). Gleiche Leistung, weniger Speicherbedarf.

        Funktioniert das auch zuverlässig mit großen Datenpaketen?

        Mit readfile(), ja.

        dedlfix.

        1. Hello,

          Ja, aber readfile() ist die bessere Alternative.

          ... und kümmert sich um Chunked?

          Das würde mich jetzt überraschen!

          Liebe Grüße
          Tom S.

          --
          Es gibt nichts Gutes, außer man tut es!
          Das Leben selbst ist der Sinn.
          1. Tach!

            Ja, aber readfile() ist die bessere Alternative.

            ... und kümmert sich um Chunked?

            Wenn du damit Transfer-Encoding: chunked meinst, vermutlich nicht. Aber eine Datei ist kein Stream und hat davon keine Vorteile.

            dedlfix.

            1. Hello,

              längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

              Dies zumindest als Info von einem Blödian

              Liebe Grüße
              Tom S.

              --
              Es gibt nichts Gutes, außer man tut es!
              Das Leben selbst ist der Sinn.
              1. Tach!

                Dies zumindest als Info von einem Blödian

                Könntest du damit bitte wieder aufhören und auf die sachliche Ebene zurückkommen?

                längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                Nochmal konkret gefragt, meinst du Transfer-Encoding: chunked aus dem HTTP oder meinst du dass eine Datei chunkweise gelesen und in den Ausgabekanal geschrieben wird?

                Wenn du Transfer-Encoding: chunked meinst, welchen Vorteil siehst du darin und was ist, wenn man das nicht verwendet?

                dedlfix.

              2. längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                Da fällt mir kein offensichtlicher Vorteil ein. Aber vielleicht weißt Du einen und kannst mir auf die Sprünge helfen.

                Interessanter wäre aus meiner Sicht (die beschriebenen zip-Dateien könnten ja sehr groß sein...) eine Möglichkeit, einen z.B. durch Verbindungsverlust abgebrochenen Download fortzusetzen zu können (siehe wget -c, diverse "Downloadmanager" oder die "Download pausieren/fortsetzen"-Funktion mancher Browser).

                Dazu müsste man sich wohl mit der Auswertung des Request-Headers und sich sodann mit fopen(), fseek(), fread() befassen und auch verhindern, dass der Download einer inzwischen geänderten Datei fortgesetzt wird (z.B. mittels ETAG oder einem ähnlichem Verfahren). Ob man dem Server den Stress zumutet ist wohl eine Frage der konkreten Aufgabe.

              3. He Tom,

                längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                Wo hast Du denn diesen Unsinn her?

                Ansonsten:

                header('Content-Length: ' . filesize($zipfile));

                heißt dass der Server die Dateilänge ja kennt und demzufolge gar kein Transfer-Encoding: chunked macht.

                MfG

                1. Hello,

                  längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                  Wo hast Du denn diesen Unsinn her?

                  Zum Beispiel aus einem Forum im Internet.

                  Eine Bitte an Dich:
                  Bevor Du hier unbegründet bewertende Bemerkungen ablässt ('Unsinn'), denke bitte selber erst noch einmal nach.

                  Liebe Grüße
                  Tom S.

                  --
                  Es gibt nichts Gutes, außer man tut es!
                  Das Leben selbst ist der Sinn.
                  1. Hi,

                    längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                    Wo hast Du denn diesen Unsinn her?

                    Zum Beispiel aus einem Forum im Internet.

                    Als Begründung für ein "heutzutage" einen Thread zu zitieren, der ein Jahrzehnt alt ist, ist - ich sag's mal vorsichtig - seltsam.

                    cu,
                    Andreas a/k/a MudGuard

                    1. Hello,

                      Als Begründung für ein "heutzutage" einen Thread zu zitieren, der ein Jahrzehnt alt ist, ist - ich sag's mal vorsichtig - seltsam.

                      Es ging nicht um "heutzutage", sondern um "Unsinn".

                      Da es Chunked Transfer auch heutzutage noch gibt, habe ich den ersten Link erwähnt, den mir Google auf "Übertragung Chunked" ausgespuckt hat.

                      Und ich finde es mehr als lästig, dass zwar mitgepostet wird, aber in keiner Weise an einer Verbesserung der Aussagen mitgearbeitet wird. Es wird nur herumgepoltert und herumgehackt. Nenne mir bessere Links zum Thema und es es kann weitergehen mit der kontinuierlichen Verbesserung!

                      Liebe Grüße
                      Tom S.

                      --
                      Es gibt nichts Gutes, außer man tut es!
                      Das Leben selbst ist der Sinn.
                      1. Da es Chunked Transfer auch heutzutage noch gibt, habe ich den ersten Link erwähnt, den mir Google auf "Übertragung Chunked" ausgespuckt hat.

                        Einfach so nach einem Begriff zu googeln und das Ergebnis als Fakt hinzustellen ist wenig hilfreich.

                        längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                        Gegenbeispiel:

                        http://www.pdflib.com/fileadmin/pdflib/pdf/manuals/PDFlib-9.1.1-tutorial.pdf

                        sendet "Content-Length:4365047" und wird dennoch sequentiell geladen. EOD

                    2. Zum Beispiel aus einem Forum im Internet.

                      Als Begründung für ein "heutzutage" einen Thread zu zitieren, der ein Jahrzehnt alt ist, ist - ich sag's mal vorsichtig - seltsam.

                      Wikipedia; Alter der HTTP-Versionen

                      Naja. Deine Vorsicht erscheint mir durchaus angebracht. HTTP/1.1 stammt aus dem Jahr 1999 und ist durchaus noch sehr relevant. Wenn sich an den Grundlagen nichts geändert hat, dann kann man durchaus auf die frühere - hier wohl auch zielführende - Diskussion in einem seriösen Forum im und über "das Internet" verweisen.

                  2. He Tom,

                    wenn ich Dir jetzt sage Du solltest Dich mal selbst um Dein Wissen in Sachen HTTP bemühen ist das durchaus begründet.

                    längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                    Nochmal, nur anders ausgedrückt: Transfer-Encoding: chunked hat weder mit PDF noch mit irgendwelchen anderen Dateiformaten etwas zu tun.

                    MfG

                    1. Hello,

                      wenn ich Dir jetzt sage Du solltest Dich mal selbst um Dein Wissen in Sachen HTTP bemühen ist das durchaus begründet.

                      Das Wissen ist ganz in Ordnung.
                      Dagegen, dass Du mir das Wort im Munde umdrehen willst, hilft auch HTTP nicht - leider! Aber vielleicht hätte eine chunked Übertragung geholfen. Dann hätte ich meinen Client nämlich bereits nach dem ersten Chunk abbrechen lassen!

                      längere PDF-Dokumente werden heutzutage üblicherweise chunked übertragen

                      Nochmal, nur anders ausgedrückt: Transfer-Encoding: chunked hat weder mit PDF noch mit irgendwelchen anderen Dateiformaten etwas zu tun.

                      Dann denk doch einfach mit und steiche einfach das "PDF" in dem Satz. Es ging um längere Dokumente, deren vollständige Übertragung zum Client keine Voraussetzung ist, damit man sie bereits zu lesen anfangen kann. PDF steht dabei in der Liste der geeigneten Formate.

                      Der Unterschied zwischen der "häppchenweisen" Übertragung per HTTP auf einen Request und der chunkweisen Übertragung auf Anforderung (mehrere Requests) ist hoffentlich bekannt.

                      Liebe Grüße
                      Tom S.

                      --
                      Es gibt nichts Gutes, außer man tut es!
                      Das Leben selbst ist der Sinn.
                      1. Hallo TS,

                        Es ging um längere Dokumente, deren vollständige Übertragung zum Client keine Voraussetzung ist, damit man sie bereits zu lesen anfangen kann. PDF steht dabei in der Liste der geeigneten Formate.

                        Ich hatte schon mehrfach PDF-Dokumente, die beim Scrollen zu einer bestimmten Seite noch die Lade-Animation zeigen.

                        Bis demnächst
                        Matthias

                        --
                        Rosen sind rot.
                        1. Ich hatte schon mehrfach PDF-Dokumente, die beim Scrollen zu einer bestimmten Seite noch die Lade-Animation zeigen.

                          Das hat aber mit dem Transfer-Encoding auch nichts zu tun, sondern ist allein eine Sache der PDF Erweiterung des Browsers. MfG

                          1. Ich hatte schon mehrfach PDF-Dokumente, die beim Scrollen zu einer bestimmten Seite noch die Lade-Animation zeigen.

                            Das hat aber mit dem Transfer-Encoding auch nichts zu tun, sondern ist allein eine Sache der PDF Erweiterung des Browsers. MfG

                            Nicht nur. Würde das PDF z.B. gzip komprimiert gesendet (oder über einen Proxy, je nachdem, wie der arbeitet), hätte der Client keine Chance dazu.

                          2. Hallo pl,

                            Ich hatte schon mehrfach PDF-Dokumente, die beim Scrollen zu einer bestimmten Seite noch die Lade-Animation zeigen.

                            Das hat aber mit dem Transfer-Encoding auch nichts zu tun, sondern ist allein eine Sache der PDF Erweiterung des Browsers. MfG

                            Nein, meine Antwort bezog sich auf

                            Dokumente, deren vollständige Übertragung zum Client keine Voraussetzung ist, damit man sie bereits zu lesen anfangen kann. PDF steht dabei in der Liste der geeigneten Formate.

                            Und nun stelle ich fest, dass ich das Wort keine überlesen habe.

                            Bis demnächst
                            Matthias

                            --
                            Rosen sind rot.
                      2. Dann denk doch einfach mit und steiche einfach das "PDF" in dem Satz. Es ging um längere Dokumente, deren vollständige Übertragung zum Client keine Voraussetzung ist, damit man sie bereits zu lesen anfangen kann. PDF steht dabei in der Liste der geeigneten Formate.

                        Auch da muss ich Dich wieder korrigieren: Mit der Länge hat Transfer-Encoding: chunked nämlich auch nichts zu tun.

                        Und mit dem Client oder Request auch nichts. Warum ein Server chunked sendet, erklärt allein der Standard CGI/1.1 als Bestandteil von HTTP. So kann ein Webserver, der eine Datei, egal ob gepuffert oder nicht, aus STDIN (von PHP) bekommt gar nicht wissen wie lang die ist, es sei denn, er bekommt das mitgeteilt (header("Content-Length: 123"), auch von PHP). Ansonsten liest der Webserver solange aus STDIN wie Daten kommen und puffert auf seine Art und Weise die Ausgabe nach STDOUT -- eben chunked.

                        So, jetzt hast Du wieder Stoff. Die RFCs dazu wirst Du sicher selber finden. MfG

                        1. Ansonsten liest der Webserver solange aus STDIN wie Daten kommen und puffert auf seine Art und Weise die Ausgabe nach STDOUT -- eben chunked.

                          Sorry, nicht STDOUT sondern in das Socket richtung Client. CGI/1.1 definiert den Common Gateway wie folgt für den Webserver:

                          • STDIN ist der Kanal aus dem der Webserver Daten liest die vom nachgelagerten Prozess (Perl, PHP..) gesendet wurden. PHP und Perl schicken Daten also nach STDOUT.

                          • STDOUT ist der Kanal in den der Webserver Daten an den nachgelagerten Prozess sendet. Z.B. einen HTTP Message Body der infolge POST gesendet wurde. Der nachgelagerte Prozess wiederum liest diesen Message Body aus STDIN.

                          Des Weiteren werden von PHP gesendete header() vom Webserver geparst und nicht etwa direkt an den Client durchgereicht. Vielmehr generiert der Webserver selbst einen RFC gerechten Header Block den er in das Socket richtung Bowser schreibt.

                          Schönen Sonntag.

  2. Im Prinzip geht das, so lange der User (unter dem der Webserver läuft) die Dateien lesen kann. Unter Sicherheitsaspekten besehen ist neben der Authentifizierung und Prüfung eventueller individueller Zugriffsberechtigung(en) insbesondere die Prüfung des Namens der angeforderten Datei bzw. Ressource ein Job, den jemand programmieren sollte, der sehr genau weiß was er da tut.

    Sonst steht man Montags bei heise.de

    Dein Skript muss folgendes können:

    1. Benutzer authentifizieren (lassen)
    2. Zugriff erlauben oder verbieten
    3. Übergebenen Dateiname/Adresse UMFASSEND prüfen (sind nicht erlaubte Zeichen oder Zeichenfolgen wie "../" oder "..\" drin?), eventuell Abbruch
    4. Überprüfen der Existenz der Datei, eventuell Abbruch, eventuell "Umbau" des Dateinamens
    5. Ermitteln des Content-Types z.b. mit finfo_file()
    6. Senden des Content-Types und weiterer Angaben mit header()
    7. Senden der Datei z.B. mit readfile()
    1. Tach!

      Dein Skript muss folgendes können:

      Nicht unbedingt, aber das wäre eine Vorgehensweise, wenn man wiederverwendbaren Code schreiben möchte. Für einen konkreten Einzelfall kann man auch Dinge wie Content-Type ermitteln weglassen, wenn nur eine einzelne Sorte von Dateien ausgeliefert werden soll.

      Ein paar Ergänzungen:

      1. Benutzer authentifizieren (lassen)
      2. Zugriff erlauben oder verbieten

      Macht gegebenenfalls der IIS bereits außerhalb und vor dem PHP-Script-Aufruf (z.B. NTLM-Authentification). Oder der Apache über .htaccess.

      1. Übergebenen Dateiname/Adresse UMFASSEND prüfen (sind nicht erlaubte Zeichen oder Zeichenfolgen wie "../" oder "..\" drin?), eventuell Abbruch

      Statt selbst nach den Punkten Ausschau zu halten, kann man realpath() verwenden. Und anschließend schauen, ob der Anfang vom absolut aufgelösten Dateinamen mit dem Verzeichnis übereinstimmt, dessen Inhalt ausgeliefert werden soll. Beim Selbst-Aufstellen der Regeln für die Punkte muss man sich gewiss sein, dass man nichts vergisst. realpath() macht jedoch den Job schon sehr viel länger.

      dedlfix.

      1. Ein paar Ergänzungen:

        1. Benutzer authentifizieren (lassen)
        2. Zugriff erlauben oder verbieten

        Macht gegebenenfalls der IIS bereits außerhalb und vor dem PHP-Script-Aufruf (z.B. NTLM-Authentification). Oder der Apache über .htaccess.

        Die Problemstellung wurde im konkreten Einzelfall mit "Das Login läuft ganz normal über PHP und Sessions." beschrieben. Darauf habe ich mich bezogen. Entschuldige bitte, dass ich nicht ein umfassendes Werk veröffentlicht habe, mit welchem sich jedes Problem unter allen denkbaren Umständen lösen lässt.

        realpath() macht jedoch den Job schon sehr viel länger.

        Das stimmt. Aber es reicht eben gerade nicht. Wenn Du schon (entgegen der konkreten Problemstellung) auf den Apache verweist, dann sollte man auch mindestens Dateien und Verzeichnisse nicht (oder nicht ohne spezielle Konfiguration) ausliefern, welche mit '.ht' beginnen, weil der Apache das in der Standardkonfiguration so vorsieht und sich Entwickler oder eben Dritte, welche die auszuliefernden Dateien in das Dateisystem einstellen, womöglich gewohnheitsmäßig darauf verlassen. Die von mir mit "UMFASSEND" beschriebene Prüfung verlangt also mehr.

        1. Hello,

          1. Zugriff erlauben oder verbieten

          Das stimmt. Aber es reicht eben gerade nicht. Wenn Du schon (entgegen der konkreten Problemstellung) auf den Apache verweist, dann sollte man auch mindestens Dateien und Verzeichnisse nicht (oder nicht ohne spezielle Konfiguration) ausliefern, welche mit '.ht' beginnen,

          .ht in der Standardkonfiguration bezieht sich leider nur auf FILES, also Dateien.

          weil der Apache das in der Standardkonfiguration so vorsieht und sich Entwickler oder eben Dritte, welche die auszuliefernden Dateien in das Dateisystem einstellen, womöglich gewohnheitsmäßig darauf verlassen. Die von mir mit "UMFASSEND" beschriebene Prüfung verlangt also mehr.

          Es sind aber nur drei Zeilen, um .ht auch auf Directories auszudehnen, was ich persönlich sehr wertschätze und daher meistens nachtrage. So kann man dann in klassisch aufgebauten Sites (in Directories organisiert) schnell mal ein ganzes Kapitel verschwinden lassen oder austauschen, ohne das alte schon löschen zu müssen.

          Liebe Grüße
          Tom S.

          --
          Es gibt nichts Gutes, außer man tut es!
          Das Leben selbst ist der Sinn.
  3. Hello,

    Das Login läuft ganz normal über PHP und Sessions. Kann ich via PHP Dateien aus geschützen Verzeichnissen weiterreichen und so zum Download anbieten? Wäre hier Curl ein Ansatz?

    Na klar!

    Stichworte blieben dann aber "Chunked Download", Verschlüsselung, Berechtigungsprüfung.

    Speziell um den Chunked Download würde sich _sonst_ vermutlich der Webserver selber kümmern.

    Liebe Grüße
    Tom S.

    --
    Es gibt nichts Gutes, außer man tut es!
    Das Leben selbst ist der Sinn.
    1. Stichworte blieben dann aber "Chunked Download",

      Meinst Du Transfer-Encoding: chunked ? Eine chunked Response wieder zusammensetzen ist kein Problem, der Algorithmus ist in einschlägigen RFCs beschrieben; den sollte jeder UA ohnehin können. Im Übrigen kannst Du das chunked ja abstellen dadurch dass du dem Webserver in einem Responseheader Content-Length mitteilst wieviele bytes da kommen.

      MfG

      1. Hello,

        Stichworte blieben dann aber "Chunked Download",

        Meinst Du Transfer-Encoding: chunked ? Eine chunked Response wieder zusammensetzen ist kein Problem, der Algorithmus ist in einschlägigen RFCs beschrieben; den sollte jeder UA ohnehin können. Im Übrigen kannst Du das chunked ja abstellen dadurch dass du dem Webserver in einem Responseheader Content-Length mitteilst wieviele bytes da kommen.

        Ist Dir gar nich in den Sinn gekommen, dass es hier genau um das Gegenteil ging?

        Wennn man Serverfunktionalitäten "zu Fuß" nachempfindet (emuliert), verliert man in aller Regel die ganzen schönen Extras: Chunked, Last_Modified, Redirections, ... und muss sie seinen Scripten selber wieder beibringen.

        Liebe Grüße
        Tom S.

        --
        Es gibt nichts Gutes, außer man tut es!
        Das Leben selbst ist der Sinn.
        1. Im Übrigen kannst Du das chunked ja abstellen dadurch dass du dem Webserver in einem Responseheader Content-Length mitteilst wieviele bytes da kommen.

          Ist Dir gar nich in den Sinn gekommen, dass es hier genau um das Gegenteil ging?

          Das Gegenteil:

          Du kannst das chunked erzwingen, in dem Du dem Server NICHT mitteilst, wie viele Bytes da kommen.

          Also statt:

          <?php
          header( 'Content-Type: application/unknown' );
          header( 'Content-Disposition: attachment; filename=' . $file );
          header( 'Content-Length: ' . filesize( $file) );
          readfile( $file );
          ?>
          

          Ergebnis (Nur header):

          ---response begin---
          HTTP/1.1 200 OK
          Date: Sat, 02 Sep 2017 13:59:01 GMT
          Server: Apache/2.4.18 (Ubuntu)
          Content-Disposition: attachment; filename=big_random
          Content-Length: 1048576
          Keep-Alive: timeout=5, max=100
          Connection: Keep-Alive
          Content-Type: application/unknown
          
          ---response end---
          

          eben

          <?php
          header( 'Content-Type: application/unknown' );
          header( 'Content-Disposition: attachment; filename=' . $file );
          # header( 'Content-Length: ' . filesize( $file) );
          readfile( $file );
          ?>
          

          Ergebnis (Nur header):

          ---response begin---
          HTTP/1.1 200 OK
          Date: Sat, 02 Sep 2017 13:57:30 GMT
          Server: Apache/2.4.18 (Ubuntu)
          Content-Disposition: attachment; filename=big_random
          Keep-Alive: timeout=5, max=100
          Connection: Keep-Alive
          Transfer-Encoding: chunked
          Content-Type: application/unknown
          
          ---response end---
          

          Eines noch: Die Datenübertragungsrate bricht bei mir (wget -d --delete-after http://localhost/...) bei einer großen Datei (1GB) von 592 MB/s auf 482MB/s ein, wenn ich chunked verwende. Das will man also tatsächlich nicht immer. Bei den angesprochenen zip-Files ist davon abzuraten, also zuzuraten, die Filesize zu lesen und zu senden.

          Spätestens bei solchen Größen würde dann die Möglichkeit einer Fortsetzung des Downloads interessant.

          1. Du kannst das chunked erzwingen, in dem Du dem Server NICHT mitteilst, wie viele Bytes da kommen.

            Korrekt. Was Deine erstklassigen Beispiele auch anschaulich zeigen. Sehr gut, ausgezeichnet!

            Freundschaft 😉

  4. Kann ich via PHP Dateien aus geschützen Verzeichnissen weiterreichen und so zum Download anbieten?

    Ja, Du dazu musst Du jedoch 2 Requests feuern. Im Ersten die Anmeldung (Login) und mit dem Zweiten dann das Download. Beide Requests müssen nur denselben Cookie senden. MfG

    1. Beide Requests müssen nur denselben Cookie senden.

      Nur zur Klarstellung: Das kann auch das bereits vorhandene Session-Cookie sein. Nicht, dass jetzt jemand hingeht und ein Extra-Cookie "strickt".

      1. Nur zur Klarstellung: Das kann auch das bereits vorhandene Session-Cookie sein. Nicht, dass jetzt jemand hingeht und ein Extra-Cookie "strickt".

        Ja da hast Du vollkommen Recht, natürlich muss das der Sessioncookie sein. Also brauchen wir noch einen Request vorher um zu gucken wie der heißt 😉