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