From 2eeb9e441b99e390067cb5573d858c8bd72902f1 Mon Sep 17 00:00:00 2001
From: ulrich
Date: Sat, 27 Mar 2021 11:29:28 +0000
Subject: [PATCH] Unterscheidung HEAD/GET eingebaut

---
 src/de/uhilger/minsrv/handler/FileHandler.java |  421 ++++++++++++++++++++++++++++++++--------------------
 1 files changed, 256 insertions(+), 165 deletions(-)

diff --git a/src/de/uhilger/minsrv/handler/FileHandler.java b/src/de/uhilger/minsrv/handler/FileHandler.java
index 5cf31b2..035ad25 100644
--- a/src/de/uhilger/minsrv/handler/FileHandler.java
+++ b/src/de/uhilger/minsrv/handler/FileHandler.java
@@ -1,31 +1,33 @@
 /*
-    mc2 - Mediacenter neu
-    Copyright (C) 2021  Ulrich Hilger
+  mini-server - Ein minimalistischer HTTP-Server
+  Copyright (C) 2021  Ulrich Hilger
 
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU Affero General Public License as
+  published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+  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 Affero General Public License for more details.
 
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+  You should have received a copy of the GNU Affero General Public License
+  along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 package de.uhilger.minsrv.handler;
 
 import com.sun.net.httpserver.Headers;
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
+import de.uhilger.minsrv.Server;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -34,179 +36,200 @@
 import java.util.logging.Logger;
 
 /**
- * Die Klasse FileHandler dient zur Auslieferung von Dateiinhalten 
- * &uuml;ber HTTP.
- * 
- * @author ulrich
+ * Die Klasse FileHandler dient zur Auslieferung von Dateiinhalten &uuml;ber
+ * HTTP.
+ *
+ * F&uuml;r das Streaming &uuml;ber HTTP wird die Auslieferung von Teilinhalten
+ * mit dem Accept-Ranges-Header angeboten und via Range-Header unterst&uuml;tzt.
+ * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
+ *
+ * @author Ulrich Hilger
  * @version 0.1, 25. M&auml;rz 2021
  */
 public class FileHandler implements HttpHandler {
-  
+
+  /* Der Logger fuer diesen FileHandler */
   private static final Logger logger = Logger.getLogger(FileHandler.class.getName());
 
-  final static String RANGE_HEADER = "Range";
-  final static String CONTENT_RANGE_HEADER = "Content-Range";
-  final static String ACCEPT_RANGES_HEADER = "Accept-Ranges";
-  final static String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
-  final static String CONTENT_TYPE = "Content-Type";
-  
+  /* Headernamen */
+  public static final String RANGE_HEADER = "Range";
+  public static final String CONTENT_RANGE_HEADER = "Content-Range";
+  public static final String ACCEPT_RANGES_HEADER = "Accept-Ranges";
+  public static final String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
+  public static final String CONTENT_TYPE = "Content-Type";
+
+  /* Statuscodes */
+  public static final int SC_OK = 200;
   public static final int SC_PARTIAL_CONTENT = 206;
+  public static final int SC_NOT_FOUND = 404;
+
+  /* HTTP Methoden */
+  public static final String HTTP_GET = "GET";
   
-  private final String basePath;
+  /* String Konstanten */
+  public static final String STR_BYTES = "bytes";
+  public static final String STR_BLANK = " ";
+  public static final String STR_DASH = "-";
+  public static final String STR_COMMA = ",";
+  public static final String STR_DOT = ".";
+  public static final String STR_NOT_FOUND = " not found.";
+  public static final String LM_PATTERN = "EEE, dd MMM yyyy HH:mm:ss zzz";
+  public static final String RANGE_PATTERN = "[^\\d-,]";
+  public static final String WELCOME_FILE = "index.html";
+
+  /* Ablageort fuer Webinhalte */
+  private final String fileBase;
 
   /**
    * Ein neues Objekt der Klasse FileHandler erzeugen
-   * 
-   * @param basePath der Pfad zu Inhalten, die von diesem Handler 
-   * ausgeliefert werden
+   *
+   * @param absoluteDirectoryPathAndName der absolute Pfad und Name des 
+   * Ordners im Dateisystem, der die Inhalte enthaelt, die von diesem 
+   * Handler ausgeliefert werden sollen
    */
-  public FileHandler(String basePath) {
-    this.basePath = basePath;
+  public FileHandler(String absoluteDirectoryPathAndName) {
+    this.fileBase = absoluteDirectoryPathAndName;
   }
 
   /**
-   * Die Datei ermitteln, die sich aus dem angefragten URL ergibt,
-   * pr&uuml;fen, ob die Datei existiert und den Inhalt der Datei 
-   * abh&auml;ngig davon, ob ein Range-Header vorhanden ist, 
-   * ganz oder teilweise ausliefern.
-   * 
-   * @param e  das Objekt mit Methoden zur Untersuchung 
-   * der Anfrage sowie zum Anfertigen und Senden der Antwort
-   * @throws IOException falls etwas schief geht entsteht dieser Fehler 
+   * Die Datei ermitteln, die sich aus dem angefragten URL ergibt, pr&uuml;fen,
+   * ob die Datei existiert und den Inhalt der Datei abh&auml;ngig davon, ob ein
+   * Range-Header vorhanden ist, ganz oder teilweise ausliefern.
+   *
+   * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
+   * Anfertigen und Senden der Antwort
+   * @throws IOException falls etwas schief geht entsteht dieser Fehler
    */
   @Override
   public void handle(HttpExchange e) throws IOException {
+    String fName = getFileName(e);
+    if (fName.startsWith(STR_DOT)) {
+      sendNotFound(e, fName);
+    } else {
+      Headers headers = e.getRequestHeaders();
+      if (headers.containsKey(RANGE_HEADER)) {
+        serveFileParts(e, new File(fileBase, fName));
+      } else {
+        if (fName.endsWith(Server.STR_SLASH)) {
+          fName += WELCOME_FILE;
+        }
+        serveFile(e, new File(fileBase, fName));
+      }
+    }
+  }
+
+  protected String getFileName(HttpExchange e) {
     String ctxPath = e.getHttpContext().getPath();
     String uriPath = e.getRequestURI().getPath();
-    String fName = uriPath.substring(ctxPath.length());
-    if(fName.startsWith(".")) {
-      throw new IOException("Mit einem Punkt beginnende Dateinamen sind ungueltig.");
-    }
-    Headers headers = e.getRequestHeaders();
-    if (headers.containsKey(RANGE_HEADER)) {
-      logger.info("has range header");
-      File file = new File(basePath, fName);
-      logger.info(file.getAbsolutePath());
-      serveFileParts(e, file);
-    } else {
-      logger.info("no range header");
-      if (fName.endsWith("/")) {
-        fName += "index.html";
-      }
-      File file = new File(basePath, fName);
-      serveFile(e, file);
-    }
+    logger.info(uriPath);
+    return uriPath.substring(ctxPath.length());
   }
-
+  
   /**
    * Den Inhalt einer Datei ausliefern
-   * 
-   * @param e  das Objekt mit Methoden zur Untersuchung 
-   * der Anfrage sowie zum Anfertigen und Senden der Antwort
+   *
+   * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
+   * Anfertigen und Senden der Antwort
    * @param file die Datei, deren Inhalt ausgeliefert werden soll
-   * @throws IOException falls etwas schief geht entsteht dieser Fehler 
+   * @throws IOException falls etwas schief geht entsteht dieser Fehler
    */
-  private void serveFile(HttpExchange e, File file) throws IOException {
-    OutputStream os = e.getResponseBody();
+  protected void serveFile(HttpExchange e, File file) throws IOException {
     if (file.exists()) {
-      e.sendResponseHeaders(200, file.length());
-      InputStream in = new FileInputStream(file);
-      int b = in.read();
-      while (b > -1) {
-        os.write(b);
-        b = in.read();
+      OutputStream os = e.getResponseBody();
+      setHeaders(e, file);
+      e.sendResponseHeaders(SC_OK, file.length());
+      if(HTTP_GET.equalsIgnoreCase(e.getRequestMethod())) {
+        InputStream in = new FileInputStream(file);
+        int b = in.read();
+        while (b > -1) {
+          os.write(b);
+          b = in.read();
+        }
+        in.close();
+        os.flush();
+        os.close();
       }
-      in.close();
     } else {
-      String response = file.getName() + " not found.";
-      byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
-      e.sendResponseHeaders(404, bytes.length);
-      os.write(bytes);
+      sendNotFound(e, file.getName());
     }
-    os.flush();
-    os.close();
   }
-  
+
   /**
    * Einen Teil des Inhalts einer Datei ausliefern
-   * 
-   * @param e  das Objekt mit Methoden zur Untersuchung 
-   * der Anfrage sowie zum Anfertigen und Senden der Antwort
+   *
+   * Wenn eine Range angefragt wird, hat die Antwort einen Content-Range Header
+   * wie folgt:
+   *
+   * <code>
+   * Content-Range: bytes 0-1023/146515
+   * Content-Length: 1024
+   * </code>
+   *
+   * Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere Content-Range
+   * Header als Multipart Response. Multipart Responses fehlen dieser
+   * Implementierung noch.
+   *
+   * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
+   *
+   * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
+   * Anfertigen und Senden der Antwort
    * @param file die Datei, deren Inhalt teilweise ausgeliefert werden soll
-   * @throws IOException falls etwas schief geht entsteht dieser Fehler 
+   * @throws IOException falls etwas schief geht entsteht dieser Fehler
    */
   /*
-    Wenn eine Range angefragt wird, hat die Antwort einen 
-    Content-Range Header wie folgt:
-
-    Content-Range: bytes 0-1023/146515
-    Content-Length: 1024
-  
-    Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere 
-    Content-Range Header als Multipart Response. Multipart Responses fehlen 
-    dieser Implementierung noch.
-  
-    (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
-  */
-  private void serveFileParts(HttpExchange e, File file) throws IOException {
-    InputStream is = new FileInputStream(file);
-    OutputStream os = e.getResponseBody();
-    Headers resHeaders = e.getResponseHeaders();
-    long responseLength = 0;
-    long start = 0;
-    long end;
-    RangeGroup rangeGroup = parseRanges(e, file);
-    Iterator<Range> i = rangeGroup.getRanges();
-    while(i.hasNext()) {
-      Range range = i.next();
-      start = range.getStart();
-      end = range.getEnd();
-      StringBuilder sb = new StringBuilder();
-      sb.append("bytes ");
-      sb.append(range.getStart());
-      sb.append("-");
-      sb.append(range.getEnd());
-      sb.append("/");
-      sb.append(file.length());
-      resHeaders.add(CONTENT_RANGE_HEADER, sb.toString());      
-      logger.info(sb.toString());
-      responseLength += (end - start);
-      logger.info("responseLength: " + responseLength);
+   */
+  protected void serveFileParts(HttpExchange e, File file) throws IOException {
+    if (file.exists()) {
+      InputStream is = new FileInputStream(file);
+      OutputStream os = e.getResponseBody();
+      setHeaders(e, file);
+      long responseLength = 0;
+      long start = 0;
+      long end;
+      RangeGroup rangeGroup = parseRanges(e, file);
+      Iterator<Range> i = rangeGroup.getRanges();
+      Headers resHeaders = e.getResponseHeaders();
+      while (i.hasNext()) {
+        Range range = i.next();
+        start = range.getStart();
+        end = range.getEnd();
+        resHeaders.add(CONTENT_RANGE_HEADER, contentRangeHdr(range, file));
+        responseLength += (end - start);
+      }
+      e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
+      if(HTTP_GET.equalsIgnoreCase(e.getRequestMethod())) {
+        if (start > 0) {
+          is.skip(start);
+        }
+        long count = 0;
+        int byteRead = is.read();
+        while (byteRead > -1 && count < responseLength) {
+          ++count;
+          os.write(byteRead);
+          byteRead = is.read();
+        }
+        os.flush();
+        os.close();
+        is.close();
+      }
+    } else {
+      sendNotFound(e, file.getName());
     }
-    resHeaders.add(CONTENT_TYPE, "video/mp4");    
-    SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
-    Date date = new Date(file.lastModified());
-    resHeaders.add(LAST_MODIFIED_DATE_HEADER, sdf.format(date));
-    e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
-    if(start > 0) {
-      is.skip(start);
-    }
-    long count = 0;
-    int byteRead = is.read();
-    while(byteRead > -1 && count < responseLength) {
-      ++count;
-      os.write(byteRead);
-      byteRead = is.read();
-    }    
-    os.flush();
-    os.close();
-    is.close();
   }
-  
+
   /**
    * Die Byte-Ranges aus dem Range-Header ermitteln.
-   * 
+   *
    * Der Range-Header kann unterschiedliche Abschnitte bezeichnen, Beispiele:
-   * Range: bytes=200-1000, 2000-6576, 19000-
-   * Range: bytes=0-499, -500
-   * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
-   * 
-   * @param e das Objekt mit Methoden zur Untersuchung 
-   * der Anfrage sowie zum Anfertigen und Senden der Antwort
+   * Range: bytes=200-1000, 2000-6576, 19000- Range: bytes=0-499, -500 (vgl.
+   * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
+   *
+   * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
+   * Anfertigen und Senden der Antwort
    * @param file die Datei, deren Inhalt ausgeliefert werden soll
    * @return die angefragten Byte-Ranges
    */
-  private RangeGroup parseRanges(HttpExchange e, File file) {
+  protected RangeGroup parseRanges(HttpExchange e, File file) {
     RangeGroup ranges = new RangeGroup();
     String rangeHeader = e.getRequestHeaders().get(RANGE_HEADER).toString();
 
@@ -221,10 +244,9 @@
     
       Der regulaere Ausdruck "[^\\d-,]" bezeichnet alle Zeichen, die keine 
       Ziffern 0-9, Bindestrich oder Komma sind.
-    */    
-    rangeHeader = rangeHeader.replaceAll("[^\\d-,]", "");
-    logger.info(rangeHeader);
-    
+     */
+    rangeHeader = rangeHeader.replaceAll(RANGE_PATTERN, "");
+
     /*
       Die Ranges ermitteln. 
     
@@ -242,17 +264,17 @@
       values.length < 2: Fall 3 ist gegeben
       values.length > 1 und values[0].length < 1: Fall 1 ist gegeben
       ansonsten: Fall 2 ist gegeben
-    */
-    String[] rangeArray = rangeHeader.split(",");
-    for(String rangeStr : rangeArray) {
+     */
+    String[] rangeArray = rangeHeader.split(STR_COMMA);
+    for (String rangeStr : rangeArray) {
       Range range = new Range();
-      String[] values = rangeStr.split("-");
-      if(values.length < 2) {
+      String[] values = rangeStr.split(STR_DASH);
+      if (values.length < 2) {
         // Fall 3
         range.setStart(Long.parseLong(values[0]));
         range.setEnd(file.length());
       } else {
-        if(values[0].length() < 1) {
+        if (values[0].length() < 1) {
           // Fall 1
           range.setStart(0);
           range.setEnd(Long.parseLong(values[1]));
@@ -263,19 +285,83 @@
         }
       }
       ranges.addRange(range);
-    }    
+    }
     return ranges;
   }
-  
+
   /**
-   * Eine Range
+   * Einen Content-Range Header erzeugen
+   * 
+   * @param range die Range, aus deren Inhalt der Header erzeugt werden soll
+   * @param file  die Datei, die den Inhalt liefert, der vom Header 
+   * bezeichnet wird
+   * @return der Inhalt des Content-Range Headers
+   */
+  protected String contentRangeHdr(Range range, File file) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(STR_BYTES);
+    sb.append(STR_BLANK);
+    sb.append(range.getStart());
+    sb.append(STR_DASH);
+    sb.append(range.getEnd());
+    sb.append(Server.STR_SLASH);
+    sb.append(file.length());
+    return sb.toString();
+  }
+
+  /**
+   * Die Header erzeugen, die unabh&auml;ngig davon, ob der ganze 
+   * Inhalt oder nur Teile davon ausgeliefert werden sollen, in der 
+   * Antwort stehen sollen 
+   * 
+   * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
+   * Anfertigen und Senden der Antwort
+   * @param file  die Datei, f&uuml;r die die Header gelten
+   * @throws IOException falls etwas schief geht entsteht dieser Fehler
+   */
+  protected void setHeaders(HttpExchange e, File file) throws IOException {
+    Headers resHeaders = e.getResponseHeaders();
+    resHeaders.add(ACCEPT_RANGES_HEADER, STR_BYTES);
+    String mimeType = Files.probeContentType(file.toPath());
+    if (mimeType != null) {
+      resHeaders.add(CONTENT_TYPE, mimeType);
+    }
+    SimpleDateFormat sdf = new SimpleDateFormat(LM_PATTERN);
+    Date date = new Date(file.lastModified());
+    resHeaders.add(LAST_MODIFIED_DATE_HEADER, sdf.format(date));
+  }
+
+  /**
+   * Eine nicht gefunden Antwort senden
+   *
+   * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
+   * Anfertigen und Senden der Antwort
+   * @param fname Name der Datei, die nicht gefunden wurde
+   * @throws IOException falls etwas schief geht entsteht dieser Fehler
+   */
+  protected void sendNotFound(HttpExchange e, String fname) throws IOException {
+    OutputStream os = e.getResponseBody();
+    String response = fname + STR_NOT_FOUND;
+    byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
+    e.sendResponseHeaders(SC_NOT_FOUND, bytes.length);
+    os.write(bytes);
+    os.flush();
+    os.close();
+  }
+
+  /**
+   * Eine Range bezeichnet einen zusammenh&auml;ngenden Bereich 
+   * aus Bytes, der sich aus den Bytepositionen des Beginns und Endes 
+   * des Bereiches ergibt.
    */
   class Range {
+
     private long start;
     private long end;
 
     /**
      * Den Beginn dieser Range ermitteln
+     *
      * @return Beginn dieser Range
      */
     public long getStart() {
@@ -284,6 +370,7 @@
 
     /**
      * Den Beginn dieser Range angeben
+     *
      * @param start Beginn dieser Range
      */
     public void setStart(long start) {
@@ -292,6 +379,7 @@
 
     /**
      * Das Ende dieser Range ermitteln
+     *
      * @return Ende dieser Range
      */
     public long getEnd() {
@@ -300,49 +388,52 @@
 
     /**
      * Das Ende dieser Range angeben
+     *
      * @param end Ende dieser Range
      */
     public void setEnd(long end) {
       this.end = end;
     }
   }
-  
+
   /**
    * Eine Gruppe aus Ranges
    */
   class RangeGroup {
+
     private List<Range> ranges;
     private long totalSize;
-    
+
     /**
      * Ein neues Objekt der Klasse RangeGroup erzeugen
      */
     public RangeGroup() {
       ranges = new ArrayList();
     }
-    
+
     /**
      * Dieser RangeGroup eine Range hinzufuegen.
-     * 
+     *
      * @param range die Range, die dieser RangeGroup hinzugefuegt werden soll
      */
     public void addRange(Range range) {
       ranges.add(range);
       totalSize += range.getEnd() - range.getStart();
     }
-    
+
     /**
-     * Die Gesamtgr&ouml;&szlig;e dieser RangeGroup ermitteln, also die 
-     * Summe der Anzahl von Bytes aller ihrer Ranges.
-     * 
+     * Die Gesamtgr&ouml;&szlig;e dieser RangeGroup ermitteln, also die Summe
+     * der Anzahl von Bytes aller ihrer Ranges.
+     *
      * @return die Gr&ouml;&szlig;e dieser RangeGroup in Bytes
      */
     public long getSize() {
       return totalSize;
     }
-    
+
     /**
      * Einen Iterator &uuml;ber die Ranges dieser RangeGroup abrufen
+     *
      * @return Iterator &uuml;ber die Ranges dieser RangeGroup
      */
     public Iterator<Range> getRanges() {

--
Gitblit v1.9.3