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