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