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