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