Ein minimalistischer HTTP-Server
ulrich
2021-03-26 d0bb217f9fd72ff981c1e96aac9d7d87006d7736
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;
d0bb21 23 import de.uhilger.minsrv.Server;
9c7249 24 import java.io.File;
U 25 import java.io.FileInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.nio.charset.StandardCharsets;
7f3fef 30 import java.nio.file.Files;
9c7249 31 import java.text.SimpleDateFormat;
U 32 import java.util.ArrayList;
33 import java.util.Date;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.logging.Logger;
37
38 /**
8abbcf 39  * Die Klasse FileHandler dient zur Auslieferung von Dateiinhalten &uuml;ber
U 40  * HTTP.
c21adf 41  *
U 42  * F&uuml;r das Streaming &uuml;ber HTTP wird die Auslieferung von Teilinhalten
43  * mit dem Accept-Ranges-Header angeboten und via Range-Header unterst&uuml;tzt.
7f3fef 44  * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
c21adf 45  *
8abbcf 46  * @author Ulrich Hilger
9c7249 47  * @version 0.1, 25. M&auml;rz 2021
U 48  */
49 public class FileHandler implements HttpHandler {
8abbcf 50
U 51   /* Der Logger fuer diesen FileHandler */
9c7249 52   private static final Logger logger = Logger.getLogger(FileHandler.class.getName());
U 53
8abbcf 54   /* Header Namen */
c21adf 55   public static final String RANGE_HEADER = "Range";
U 56   public static final String CONTENT_RANGE_HEADER = "Content-Range";
57   public static final String ACCEPT_RANGES_HEADER = "Accept-Ranges";
58   public static final String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
59   public static final String CONTENT_TYPE = "Content-Type";
8abbcf 60
U 61   /* Status Codes */
62   public static final int SC_OK = 200;
9c7249 63   public static final int SC_PARTIAL_CONTENT = 206;
8abbcf 64   public static final int SC_NOT_FOUND = 404;
c21adf 65
U 66   /* String Konstanten */
67   public static final String STR_BYTES = "bytes";
68   public static final String STR_BLANK = " ";
69   public static final String STR_DASH = "-";
70   public static final String STR_COMMA = ",";
71   public static final String STR_DOT = ".";
72   public static final String STR_NOT_FOUND = " not found.";
73   public static final String LM_PATTERN = "EEE, dd MMM yyyy HH:mm:ss zzz";
74   public static final String RANGE_PATTERN = "[^\\d-,]";
75   public static final String WELCOME_FILE = "index.html";
8abbcf 76
U 77   /* Ablageort fuer Webinhalte */
9c7249 78   private final String basePath;
U 79
80   /**
81    * Ein neues Objekt der Klasse FileHandler erzeugen
8abbcf 82    *
U 83    * @param basePath der Pfad zu Inhalten, die von diesem Handler ausgeliefert
84    * werden
9c7249 85    */
U 86   public FileHandler(String basePath) {
87     this.basePath = basePath;
88   }
89
90   /**
8abbcf 91    * Die Datei ermitteln, die sich aus dem angefragten URL ergibt, pr&uuml;fen,
U 92    * ob die Datei existiert und den Inhalt der Datei abh&auml;ngig davon, ob ein
93    * Range-Header vorhanden ist, ganz oder teilweise ausliefern.
94    *
95    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
96    * Anfertigen und Senden der Antwort
97    * @throws IOException falls etwas schief geht entsteht dieser Fehler
9c7249 98    */
U 99   @Override
100   public void handle(HttpExchange e) throws IOException {
101     String ctxPath = e.getHttpContext().getPath();
102     String uriPath = e.getRequestURI().getPath();
e966eb 103     logger.info(uriPath);
9c7249 104     String fName = uriPath.substring(ctxPath.length());
c21adf 105     if (fName.startsWith(STR_DOT)) {
8abbcf 106       sendNotFound(e, fName);
9c7249 107     } else {
e966eb 108       Headers headers = e.getRequestHeaders();
8abbcf 109       if (headers.containsKey(RANGE_HEADER)) {
U 110         serveFileParts(e, new File(basePath, fName));
111       } else {
d0bb21 112         if (fName.endsWith(Server.STR_SLASH)) {
c21adf 113           fName += WELCOME_FILE;
8abbcf 114         }
U 115         serveFile(e, new File(basePath, fName));
9c7249 116       }
U 117     }
118   }
119
120   /**
121    * Den Inhalt einer Datei ausliefern
8abbcf 122    *
U 123    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
124    * Anfertigen und Senden der Antwort
9c7249 125    * @param file die Datei, deren Inhalt ausgeliefert werden soll
8abbcf 126    * @throws IOException falls etwas schief geht entsteht dieser Fehler
9c7249 127    */
U 128   private void serveFile(HttpExchange e, File file) throws IOException {
129     if (file.exists()) {
8abbcf 130       OutputStream os = e.getResponseBody();
51e1d5 131       Headers headers = e.getResponseHeaders();
7f3fef 132       setCommonHeaders(headers, file);
8abbcf 133       e.sendResponseHeaders(SC_OK, file.length());
9c7249 134       InputStream in = new FileInputStream(file);
U 135       int b = in.read();
136       while (b > -1) {
137         os.write(b);
138         b = in.read();
139       }
140       in.close();
8abbcf 141       os.flush();
U 142       os.close();
9c7249 143     } else {
8abbcf 144       sendNotFound(e, file.getName());
9c7249 145     }
U 146   }
8abbcf 147
9c7249 148   /**
U 149    * Einen Teil des Inhalts einer Datei ausliefern
8abbcf 150    *
U 151    * Wenn eine Range angefragt wird, hat die Antwort einen Content-Range Header
152    * wie folgt:
153    *
154    * <code>
155    * Content-Range: bytes 0-1023/146515
156    * Content-Length: 1024
157    * </code>
158    *
159    * Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere Content-Range
160    * Header als Multipart Response. Multipart Responses fehlen dieser
161    * Implementierung noch.
162    *
163    * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
164    *
165    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
166    * Anfertigen und Senden der Antwort
9c7249 167    * @param file die Datei, deren Inhalt teilweise ausgeliefert werden soll
8abbcf 168    * @throws IOException falls etwas schief geht entsteht dieser Fehler
9c7249 169    */
U 170   /*
8abbcf 171    */
9c7249 172   private void serveFileParts(HttpExchange e, File file) throws IOException {
8abbcf 173     if (file.exists()) {
U 174       InputStream is = new FileInputStream(file);
175       OutputStream os = e.getResponseBody();
176       Headers resHeaders = e.getResponseHeaders();
7f3fef 177       setCommonHeaders(resHeaders, file);
8abbcf 178       long responseLength = 0;
U 179       long start = 0;
180       long end;
181       RangeGroup rangeGroup = parseRanges(e, file);
182       Iterator<Range> i = rangeGroup.getRanges();
183       while (i.hasNext()) {
184         Range range = i.next();
185         start = range.getStart();
186         end = range.getEnd();
c21adf 187         resHeaders.add(CONTENT_RANGE_HEADER, contentRangeHdr(range, file));
8abbcf 188         responseLength += (end - start);
U 189       }
190       e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
191       if (start > 0) {
192         is.skip(start);
193       }
194       long count = 0;
195       int byteRead = is.read();
196       while (byteRead > -1 && count < responseLength) {
197         ++count;
198         os.write(byteRead);
199         byteRead = is.read();
200       }
201       os.flush();
202       os.close();
203       is.close();
204     } else {
205       sendNotFound(e, file.getName());
9c7249 206     }
U 207   }
c21adf 208
9c7249 209   /**
U 210    * Die Byte-Ranges aus dem Range-Header ermitteln.
8abbcf 211    *
9c7249 212    * Der Range-Header kann unterschiedliche Abschnitte bezeichnen, Beispiele:
8abbcf 213    * Range: bytes=200-1000, 2000-6576, 19000- Range: bytes=0-499, -500 (vgl.
U 214    * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
215    *
216    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
217    * Anfertigen und Senden der Antwort
9c7249 218    * @param file die Datei, deren Inhalt ausgeliefert werden soll
U 219    * @return die angefragten Byte-Ranges
220    */
221   private RangeGroup parseRanges(HttpExchange e, File file) {
222     RangeGroup ranges = new RangeGroup();
223     String rangeHeader = e.getRequestHeaders().get(RANGE_HEADER).toString();
c21adf 224
9c7249 225     /*
U 226       Inhalt des Range-Headers von nicht benoetigten Angaben befreien
227     
228       Ein Range Header enthaelt neben den Start- und Endwerten der Ranges auch 
229       die Angabe "bytes:". Es ist aber keine andere Auspraegung als Bytes 
230       spezifiziert, daher muss die Angabe nicht ausgewertet werden und kann 
231       entfallen. Der Range-Header kann zudem noch eckige Klammern haben 
232       wie in [bytes=200-1000].
233     
234       Der regulaere Ausdruck "[^\\d-,]" bezeichnet alle Zeichen, die keine 
235       Ziffern 0-9, Bindestrich oder Komma sind.
8abbcf 236      */
c21adf 237     rangeHeader = rangeHeader.replaceAll(RANGE_PATTERN, "");
8abbcf 238
9c7249 239     /*
U 240       Die Ranges ermitteln. 
241     
242       Nach dem vorangegangenen Schritt besteht der Header-Ausdruck nur noch 
243       aus einer mit Kommas getrennten Liste aus Start- und Endwerten wie z.B. 
244       "-103,214-930,1647-"
245     
246       Ein Range-Ausdruck kann dann drei verschiedene Auspraegungen haben:
247       1. Startwert fehlt, z.B. -200
248       2. Start und Ende sind vorhanden, z.B. 101-200
249       3. Endwert fehlt, z.B. 201-
250       
251       Teilt man einen Range-String mit der Methode String.split("-") am 
252       Bindestrich ('-') in ein String-Array 'values' gilt:
253       values.length < 2: Fall 3 ist gegeben
254       values.length > 1 und values[0].length < 1: Fall 1 ist gegeben
255       ansonsten: Fall 2 ist gegeben
8abbcf 256      */
c21adf 257     String[] rangeArray = rangeHeader.split(STR_COMMA);
8abbcf 258     for (String rangeStr : rangeArray) {
9c7249 259       Range range = new Range();
c21adf 260       String[] values = rangeStr.split(STR_DASH);
8abbcf 261       if (values.length < 2) {
9c7249 262         // Fall 3
U 263         range.setStart(Long.parseLong(values[0]));
264         range.setEnd(file.length());
265       } else {
8abbcf 266         if (values[0].length() < 1) {
9c7249 267           // Fall 1
U 268           range.setStart(0);
269           range.setEnd(Long.parseLong(values[1]));
270         } else {
271           // Fall 2
272           range.setStart(Long.parseLong(values[0]));
273           range.setEnd(Long.parseLong(values[1]));
274         }
275       }
276       ranges.addRange(range);
8abbcf 277     }
9c7249 278     return ranges;
U 279   }
8abbcf 280
9c7249 281   /**
d0bb21 282    * Einen Content-Range Header erzeugen
U 283    * 
284    * @param range die Range, aus deren Inhalt der Header erzeugt werden soll
285    * @param file  die Datei, die den Inhalt liefert, der vom Header 
286    * bezeichnet wird
287    * @return der Inhalt des Content-Range Headers
288    */
289   private String contentRangeHdr(Range range, File file) {
290     StringBuilder sb = new StringBuilder();
291     sb.append(STR_BYTES);
292     sb.append(STR_BLANK);
293     sb.append(range.getStart());
294     sb.append(STR_DASH);
295     sb.append(range.getEnd());
296     sb.append(Server.STR_SLASH);
297     sb.append(file.length());
298     return sb.toString();
299   }
300
301   /**
302    * Die Header erzeugen, die unabh&auml;ngig davon, ob der ganze 
303    * Inhalt oder nur Teile davon ausgeliefert werden sollen, in der 
304    * Antwort stehen sollen 
305    * 
306    * @param resHeaders das Objekt, in das die Header erzeugt werden
307    * @param file  die Datei, f&uuml;r die die Header gelten
308    * @throws IOException falls etwas schief geht entsteht dieser Fehler
309    */
310   private void setCommonHeaders(Headers resHeaders, File file) throws IOException {
311     resHeaders.add(ACCEPT_RANGES_HEADER, STR_BYTES);
312     String mimeType = Files.probeContentType(file.toPath());
313     if (mimeType != null) {
314       resHeaders.add(CONTENT_TYPE, mimeType);
315     }
316     SimpleDateFormat sdf = new SimpleDateFormat(LM_PATTERN);
317     Date date = new Date(file.lastModified());
318     resHeaders.add(LAST_MODIFIED_DATE_HEADER, sdf.format(date));
319   }
320
321   /**
51e1d5 322    * Eine nicht gefunden Antwort senden
c21adf 323    *
51e1d5 324    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
U 325    * Anfertigen und Senden der Antwort
326    * @param fname Name der Datei, die nicht gefunden wurde
327    * @throws IOException falls etwas schief geht entsteht dieser Fehler
328    */
d0bb21 329   private void sendNotFound(HttpExchange e, String fname) throws IOException {
51e1d5 330     OutputStream os = e.getResponseBody();
c21adf 331     String response = fname + STR_NOT_FOUND;
51e1d5 332     byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
U 333     e.sendResponseHeaders(SC_NOT_FOUND, bytes.length);
334     os.write(bytes);
335     os.flush();
336     os.close();
337   }
338
339   /**
d0bb21 340    * Eine Range bezeichnet einen zusammenh&auml;ngenden Bereich 
U 341    * aus Bytes, der sich aus den Bytepositionen des Beginns und Endes 
342    * des Bereiches ergibt.
9c7249 343    */
U 344   class Range {
8abbcf 345
9c7249 346     private long start;
U 347     private long end;
348
349     /**
350      * Den Beginn dieser Range ermitteln
8abbcf 351      *
9c7249 352      * @return Beginn dieser Range
U 353      */
354     public long getStart() {
355       return start;
356     }
357
358     /**
359      * Den Beginn dieser Range angeben
8abbcf 360      *
9c7249 361      * @param start Beginn dieser Range
U 362      */
363     public void setStart(long start) {
364       this.start = start;
365     }
366
367     /**
368      * Das Ende dieser Range ermitteln
8abbcf 369      *
9c7249 370      * @return Ende dieser Range
U 371      */
372     public long getEnd() {
373       return end;
374     }
375
376     /**
377      * Das Ende dieser Range angeben
8abbcf 378      *
9c7249 379      * @param end Ende dieser Range
U 380      */
381     public void setEnd(long end) {
382       this.end = end;
383     }
384   }
8abbcf 385
9c7249 386   /**
U 387    * Eine Gruppe aus Ranges
388    */
389   class RangeGroup {
8abbcf 390
9c7249 391     private List<Range> ranges;
U 392     private long totalSize;
8abbcf 393
9c7249 394     /**
U 395      * Ein neues Objekt der Klasse RangeGroup erzeugen
396      */
397     public RangeGroup() {
398       ranges = new ArrayList();
399     }
8abbcf 400
9c7249 401     /**
U 402      * Dieser RangeGroup eine Range hinzufuegen.
8abbcf 403      *
9c7249 404      * @param range die Range, die dieser RangeGroup hinzugefuegt werden soll
U 405      */
406     public void addRange(Range range) {
407       ranges.add(range);
408       totalSize += range.getEnd() - range.getStart();
409     }
8abbcf 410
9c7249 411     /**
8abbcf 412      * Die Gesamtgr&ouml;&szlig;e dieser RangeGroup ermitteln, also die Summe
U 413      * der Anzahl von Bytes aller ihrer Ranges.
414      *
9c7249 415      * @return die Gr&ouml;&szlig;e dieser RangeGroup in Bytes
U 416      */
417     public long getSize() {
418       return totalSize;
419     }
8abbcf 420
9c7249 421     /**
U 422      * Einen Iterator &uuml;ber die Ranges dieser RangeGroup abrufen
8abbcf 423      *
9c7249 424      * @return Iterator &uuml;ber die Ranges dieser RangeGroup
U 425      */
426     public Iterator<Range> getRanges() {
427       return ranges.iterator();
428     }
429   }
430 }