Ein minimalistischer HTTP-Server
ulrich
2021-03-26 8abbcfbb478405ca5f61218018ac5d61104d6f35
commit | author | age
9c7249 1 /*
678b07 2   mini-server - Ein minimalistischer HTTP-Server
U 3   Copyright (C) 2021  Ulrich Hilger
9c7249 4
678b07 5   This program is free software: you can redistribute it and/or modify
U 6   it under the terms of the GNU Affero General Public License as
7   published by the Free Software Foundation, either version 3 of the
8   License, or (at your option) any later version.
9c7249 9
678b07 10   This program is distributed in the hope that it will be useful,
U 11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU Affero General Public License for more details.
9c7249 14
678b07 15   You should have received a copy of the GNU Affero General Public License
U 16   along with this program.  If not, see <https://www.gnu.org/licenses/>.
8abbcf 17  */
9c7249 18 package de.uhilger.minsrv.handler;
U 19
20 import com.sun.net.httpserver.Headers;
21 import com.sun.net.httpserver.HttpExchange;
22 import com.sun.net.httpserver.HttpHandler;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.nio.charset.StandardCharsets;
29 import java.text.SimpleDateFormat;
30 import java.util.ArrayList;
31 import java.util.Date;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.logging.Logger;
35
36 /**
8abbcf 37  * Die Klasse FileHandler dient zur Auslieferung von Dateiinhalten &uuml;ber
U 38  * HTTP.
39  *
40  * @author Ulrich Hilger
9c7249 41  * @version 0.1, 25. M&auml;rz 2021
U 42  */
43 public class FileHandler implements HttpHandler {
8abbcf 44
U 45   /* Der Logger fuer diesen FileHandler */
9c7249 46   private static final Logger logger = Logger.getLogger(FileHandler.class.getName());
U 47
8abbcf 48   /* Header Namen */
9c7249 49   final static String RANGE_HEADER = "Range";
U 50   final static String CONTENT_RANGE_HEADER = "Content-Range";
51   final static String ACCEPT_RANGES_HEADER = "Accept-Ranges";
52   final static String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
53   final static String CONTENT_TYPE = "Content-Type";
8abbcf 54
U 55   /* Status Codes */
56   public static final int SC_OK = 200;
9c7249 57   public static final int SC_PARTIAL_CONTENT = 206;
8abbcf 58   public static final int SC_NOT_FOUND = 404;
U 59
60   /* Ablageort fuer Webinhalte */
9c7249 61   private final String basePath;
U 62
63   /**
64    * Ein neues Objekt der Klasse FileHandler erzeugen
8abbcf 65    *
U 66    * @param basePath der Pfad zu Inhalten, die von diesem Handler ausgeliefert
67    * werden
9c7249 68    */
U 69   public FileHandler(String basePath) {
70     this.basePath = basePath;
71   }
72
73   /**
8abbcf 74    * Die Datei ermitteln, die sich aus dem angefragten URL ergibt, pr&uuml;fen,
U 75    * ob die Datei existiert und den Inhalt der Datei abh&auml;ngig davon, ob ein
76    * Range-Header vorhanden ist, ganz oder teilweise ausliefern.
77    *
78    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
79    * Anfertigen und Senden der Antwort
80    * @throws IOException falls etwas schief geht entsteht dieser Fehler
9c7249 81    */
U 82   @Override
83   public void handle(HttpExchange e) throws IOException {
84     String ctxPath = e.getHttpContext().getPath();
85     String uriPath = e.getRequestURI().getPath();
86     String fName = uriPath.substring(ctxPath.length());
8abbcf 87     if (fName.startsWith(".")) {
U 88       sendNotFound(e, fName);
9c7249 89     } else {
8abbcf 90       Headers headers = e.getRequestHeaders();
U 91       if (headers.containsKey(RANGE_HEADER)) {
92         serveFileParts(e, new File(basePath, fName));
93       } else {
94         if (fName.endsWith("/")) {
95           fName += "index.html";
96         }
97         serveFile(e, new File(basePath, fName));
9c7249 98       }
U 99     }
8abbcf 100   }
U 101
102   public void sendNotFound(HttpExchange e, String fname) throws IOException {
103     OutputStream os = e.getResponseBody();
104     String response = fname + " not found.";
105     byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
106     e.sendResponseHeaders(SC_NOT_FOUND, bytes.length);
107     os.write(bytes);
108     os.flush();
109     os.close();
9c7249 110   }
U 111
112   /**
113    * Den Inhalt einer Datei ausliefern
8abbcf 114    *
U 115    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
116    * Anfertigen und Senden der Antwort
9c7249 117    * @param file die Datei, deren Inhalt ausgeliefert werden soll
8abbcf 118    * @throws IOException falls etwas schief geht entsteht dieser Fehler
9c7249 119    */
U 120   private void serveFile(HttpExchange e, File file) throws IOException {
121     if (file.exists()) {
8abbcf 122       OutputStream os = e.getResponseBody();
U 123       e.sendResponseHeaders(SC_OK, file.length());
9c7249 124       InputStream in = new FileInputStream(file);
U 125       int b = in.read();
126       while (b > -1) {
127         os.write(b);
128         b = in.read();
129       }
130       in.close();
8abbcf 131       os.flush();
U 132       os.close();
9c7249 133     } else {
8abbcf 134       sendNotFound(e, file.getName());
U 135       /*
9c7249 136       String response = file.getName() + " not found.";
U 137       byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
8abbcf 138       e.sendResponseHeaders(SC_NOT_FOUND, bytes.length);
9c7249 139       os.write(bytes);
8abbcf 140        */
9c7249 141     }
U 142   }
8abbcf 143
9c7249 144   /**
U 145    * Einen Teil des Inhalts einer Datei ausliefern
8abbcf 146    *
U 147    * Wenn eine Range angefragt wird, hat die Antwort einen Content-Range Header
148    * wie folgt:
149    *
150    * <code>
151    * Content-Range: bytes 0-1023/146515
152    * Content-Length: 1024
153    * </code>
154    *
155    * Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere Content-Range
156    * Header als Multipart Response. Multipart Responses fehlen dieser
157    * Implementierung noch.
158    *
159    * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
160    *
161    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
162    * Anfertigen und Senden der Antwort
9c7249 163    * @param file die Datei, deren Inhalt teilweise ausgeliefert werden soll
8abbcf 164    * @throws IOException falls etwas schief geht entsteht dieser Fehler
9c7249 165    */
U 166   /*
8abbcf 167    */
9c7249 168   private void serveFileParts(HttpExchange e, File file) throws IOException {
8abbcf 169     if (file.exists()) {
U 170       InputStream is = new FileInputStream(file);
171       OutputStream os = e.getResponseBody();
172       Headers resHeaders = e.getResponseHeaders();
173       long responseLength = 0;
174       long start = 0;
175       long end;
176       RangeGroup rangeGroup = parseRanges(e, file);
177       Iterator<Range> i = rangeGroup.getRanges();
178       while (i.hasNext()) {
179         Range range = i.next();
180         start = range.getStart();
181         end = range.getEnd();
182         StringBuilder sb = new StringBuilder();
183         sb.append("bytes ");
184         sb.append(range.getStart());
185         sb.append("-");
186         sb.append(range.getEnd());
187         sb.append("/");
188         sb.append(file.length());
189         resHeaders.add(CONTENT_RANGE_HEADER, sb.toString());
190         logger.info(sb.toString());
191         responseLength += (end - start);
192         logger.info("responseLength: " + responseLength);
193       }
194       resHeaders.add(CONTENT_TYPE, "video/mp4");
195       SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
196       Date date = new Date(file.lastModified());
197       resHeaders.add(LAST_MODIFIED_DATE_HEADER, sdf.format(date));
198       e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
199       if (start > 0) {
200         is.skip(start);
201       }
202       long count = 0;
203       int byteRead = is.read();
204       while (byteRead > -1 && count < responseLength) {
205         ++count;
206         os.write(byteRead);
207         byteRead = is.read();
208       }
209       os.flush();
210       os.close();
211       is.close();
212     } else {
213       sendNotFound(e, file.getName());
9c7249 214     }
U 215   }
8abbcf 216
9c7249 217   /**
U 218    * Die Byte-Ranges aus dem Range-Header ermitteln.
8abbcf 219    *
9c7249 220    * Der Range-Header kann unterschiedliche Abschnitte bezeichnen, Beispiele:
8abbcf 221    * Range: bytes=200-1000, 2000-6576, 19000- Range: bytes=0-499, -500 (vgl.
U 222    * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
223    *
224    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
225    * Anfertigen und Senden der Antwort
9c7249 226    * @param file die Datei, deren Inhalt ausgeliefert werden soll
U 227    * @return die angefragten Byte-Ranges
228    */
229   private RangeGroup parseRanges(HttpExchange e, File file) {
230     RangeGroup ranges = new RangeGroup();
231     String rangeHeader = e.getRequestHeaders().get(RANGE_HEADER).toString();
232
233     /*
234       Inhalt des Range-Headers von nicht benoetigten Angaben befreien
235     
236       Ein Range Header enthaelt neben den Start- und Endwerten der Ranges auch 
237       die Angabe "bytes:". Es ist aber keine andere Auspraegung als Bytes 
238       spezifiziert, daher muss die Angabe nicht ausgewertet werden und kann 
239       entfallen. Der Range-Header kann zudem noch eckige Klammern haben 
240       wie in [bytes=200-1000].
241     
242       Der regulaere Ausdruck "[^\\d-,]" bezeichnet alle Zeichen, die keine 
243       Ziffern 0-9, Bindestrich oder Komma sind.
8abbcf 244      */
9c7249 245     rangeHeader = rangeHeader.replaceAll("[^\\d-,]", "");
U 246     logger.info(rangeHeader);
8abbcf 247
9c7249 248     /*
U 249       Die Ranges ermitteln. 
250     
251       Nach dem vorangegangenen Schritt besteht der Header-Ausdruck nur noch 
252       aus einer mit Kommas getrennten Liste aus Start- und Endwerten wie z.B. 
253       "-103,214-930,1647-"
254     
255       Ein Range-Ausdruck kann dann drei verschiedene Auspraegungen haben:
256       1. Startwert fehlt, z.B. -200
257       2. Start und Ende sind vorhanden, z.B. 101-200
258       3. Endwert fehlt, z.B. 201-
259       
260       Teilt man einen Range-String mit der Methode String.split("-") am 
261       Bindestrich ('-') in ein String-Array 'values' gilt:
262       values.length < 2: Fall 3 ist gegeben
263       values.length > 1 und values[0].length < 1: Fall 1 ist gegeben
264       ansonsten: Fall 2 ist gegeben
8abbcf 265      */
9c7249 266     String[] rangeArray = rangeHeader.split(",");
8abbcf 267     for (String rangeStr : rangeArray) {
9c7249 268       Range range = new Range();
U 269       String[] values = rangeStr.split("-");
8abbcf 270       if (values.length < 2) {
9c7249 271         // Fall 3
U 272         range.setStart(Long.parseLong(values[0]));
273         range.setEnd(file.length());
274       } else {
8abbcf 275         if (values[0].length() < 1) {
9c7249 276           // Fall 1
U 277           range.setStart(0);
278           range.setEnd(Long.parseLong(values[1]));
279         } else {
280           // Fall 2
281           range.setStart(Long.parseLong(values[0]));
282           range.setEnd(Long.parseLong(values[1]));
283         }
284       }
285       ranges.addRange(range);
8abbcf 286     }
9c7249 287     return ranges;
U 288   }
8abbcf 289
9c7249 290   /**
U 291    * Eine Range
292    */
293   class Range {
8abbcf 294
9c7249 295     private long start;
U 296     private long end;
297
298     /**
299      * Den Beginn dieser Range ermitteln
8abbcf 300      *
9c7249 301      * @return Beginn dieser Range
U 302      */
303     public long getStart() {
304       return start;
305     }
306
307     /**
308      * Den Beginn dieser Range angeben
8abbcf 309      *
9c7249 310      * @param start Beginn dieser Range
U 311      */
312     public void setStart(long start) {
313       this.start = start;
314     }
315
316     /**
317      * Das Ende dieser Range ermitteln
8abbcf 318      *
9c7249 319      * @return Ende dieser Range
U 320      */
321     public long getEnd() {
322       return end;
323     }
324
325     /**
326      * Das Ende dieser Range angeben
8abbcf 327      *
9c7249 328      * @param end Ende dieser Range
U 329      */
330     public void setEnd(long end) {
331       this.end = end;
332     }
333   }
8abbcf 334
9c7249 335   /**
U 336    * Eine Gruppe aus Ranges
337    */
338   class RangeGroup {
8abbcf 339
9c7249 340     private List<Range> ranges;
U 341     private long totalSize;
8abbcf 342
9c7249 343     /**
U 344      * Ein neues Objekt der Klasse RangeGroup erzeugen
345      */
346     public RangeGroup() {
347       ranges = new ArrayList();
348     }
8abbcf 349
9c7249 350     /**
U 351      * Dieser RangeGroup eine Range hinzufuegen.
8abbcf 352      *
9c7249 353      * @param range die Range, die dieser RangeGroup hinzugefuegt werden soll
U 354      */
355     public void addRange(Range range) {
356       ranges.add(range);
357       totalSize += range.getEnd() - range.getStart();
358     }
8abbcf 359
9c7249 360     /**
8abbcf 361      * Die Gesamtgr&ouml;&szlig;e dieser RangeGroup ermitteln, also die Summe
U 362      * der Anzahl von Bytes aller ihrer Ranges.
363      *
9c7249 364      * @return die Gr&ouml;&szlig;e dieser RangeGroup in Bytes
U 365      */
366     public long getSize() {
367       return totalSize;
368     }
8abbcf 369
9c7249 370     /**
U 371      * Einen Iterator &uuml;ber die Ranges dieser RangeGroup abrufen
8abbcf 372      *
9c7249 373      * @return Iterator &uuml;ber die Ranges dieser RangeGroup
U 374      */
375     public Iterator<Range> getRanges() {
376       return ranges.iterator();
377     }
378   }
379 }