Ultrakompakter HTTP Server
ulrich
2024-02-23 4d253a518cdd889ad84c8f873aa615e14b6d9ca8
commit | author | age
e58690 1 /*
U 2   neon - Embeddable HTTP Server based on jdk.httpserver
3   Copyright (C) 2024  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.neon;
19
20 import com.sun.net.httpserver.Headers;
21 import com.sun.net.httpserver.HttpExchange;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.util.Iterator;
28
29 /**
30  * Die Klasse PartialFileServer fuehrt die zur Auslieferung von Teilen 
31  * einer Datei noetigen Handlungen aus. 
32  * 
33  * @author Ulrich Hilger
34  * @version 1, 11.06.2021
35  */
36 public class PartialFileServer {
37   
38   public static final String CONTENT_RANGE_HEADER = "Content-Range";
39   public static final String RANGE_PATTERN = "[^\\d-,]";
40
41   public static final int SC_PARTIAL_CONTENT = 206;
42
43   /**
44    * Einen Teil des Inhalts einer Datei ausliefern
45    *
46    * Wenn eine Range angefragt wird, hat die Antwort einen Content-Range Header
47    * wie folgt:
48    *
49    * <code>
50    * Content-Range: bytes 0-1023/146515
51    * Content-Length: 1024
52    * </code>
53    *
54    * Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere Content-Range
55    * Header als Multipart Response. Multipart Responses fehlen dieser
56    * Implementierung noch.
57    *
58    * (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
59    *
60    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
61    * Anfertigen und Senden der Antwort
62    * @param file die Datei, deren Inhalt teilweise ausgeliefert werden soll
63    * @throws IOException falls etwas schief geht entsteht dieser Fehler
64    */
65   /*
66    */
67   public void serveFileParts(HttpExchange e, File file) throws IOException {
68     if (file.exists()) {
69       HttpResponder fs = new HttpResponder();
70       fs.setHeaders(e, file);
71       long responseLength = 0;
72       long start = 0;
73       long end;
74       RangeGroup rangeGroup = parseRanges(e, file);
75       Iterator<Range> i = rangeGroup.getRanges();
76       Headers resHeaders = e.getResponseHeaders();
77       while (i.hasNext()) {
78         Range range = i.next();
79         start = range.getStart();
80         end = range.getEnd();
81         resHeaders.add(CONTENT_RANGE_HEADER, contentRangeHdr(range, file));
82         responseLength += (end - start);
83       }
84       e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
85       if(HttpHelper.HTTP_GET.equalsIgnoreCase(e.getRequestMethod())) {
86         InputStream is = new FileInputStream(file);
87         OutputStream os = e.getResponseBody();
88         if (start > 0) {
89           is.skip(start);
90         }
91         long count = 0;
92         int byteRead = is.read();
93         while (byteRead > -1 && count < responseLength) {
94           ++count;
95           os.write(byteRead);
96           byteRead = is.read();
97         }
98         os.flush();
99         os.close();
100         is.close();
101       }
102     } else {
103       HttpResponder fs = new HttpResponder();
104       fs.sendNotFound(e, file.getName());
105     }
106   }
107
108   /**
109    * Die Byte-Ranges aus dem Range-Header ermitteln.
110    *
111    * Der Range-Header kann unterschiedliche Abschnitte bezeichnen, Beispiele:
112    * Range: bytes=200-1000, 2000-6576, 19000- Range: bytes=0-499, -500 (vgl.
113    * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
114    *
115    * @param e das Objekt mit Methoden zur Untersuchung der Anfrage sowie zum
116    * Anfertigen und Senden der Antwort
117    * @param file die Datei, deren Inhalt ausgeliefert werden soll
118    * @return die angefragten Byte-Ranges
119    */
120   protected RangeGroup parseRanges(HttpExchange e, File file) {
121     RangeGroup ranges = new RangeGroup();
122     String rangeHeader = e.getRequestHeaders().get(FileServer.RANGE_HEADER).toString();
123
124     /*
125       Inhalt des Range-Headers von nicht benoetigten Angaben befreien
126     
127       Ein Range Header enthaelt neben den Start- und Endwerten der Ranges auch 
128       die Angabe "bytes:". Es ist aber keine andere Auspraegung als Bytes 
129       spezifiziert, daher muss die Angabe nicht ausgewertet werden und kann 
130       entfallen. Der Range-Header kann zudem noch eckige Klammern haben 
131       wie in [bytes=200-1000].
132     
133       Der regulaere Ausdruck "[^\\d-,]" bezeichnet alle Zeichen, die keine 
134       Ziffern 0-9, Bindestrich oder Komma sind.
135      */
136     rangeHeader = rangeHeader.replaceAll(RANGE_PATTERN, "");
137
138     /*
139       Die Ranges ermitteln. 
140     
141       Nach dem vorangegangenen Schritt besteht der Header-Ausdruck nur noch 
142       aus einer mit Kommas getrennten Liste aus Start- und Endwerten wie z.B. 
143       "-103,214-930,1647-"
144     
145       Ein Range-Ausdruck kann dann drei verschiedene Auspraegungen haben:
146       1. Startwert fehlt, z.B. -200
147       2. Start und Ende sind vorhanden, z.B. 101-200
148       3. Endwert fehlt, z.B. 201-
149       
150       Teilt man einen Range-String mit der Methode String.split("-") am 
151       Bindestrich ('-') in ein String-Array 'values' gilt:
152       values.length < 2: Fall 3 ist gegeben
153       values.length > 1 und values[0].length < 1: Fall 1 ist gegeben
154       ansonsten: Fall 2 ist gegeben
155      */
156     String[] rangeArray = rangeHeader.split(FileServer.STR_COMMA);
157     for (String rangeStr : rangeArray) {
158       Range range = new Range();
159       String[] values = rangeStr.split(FileServer.STR_DASH);
160       if (values.length < 2) {
161         // Fall 3
162         range.setStart(Long.parseLong(values[0]));
163         range.setEnd(file.length());
164       } else {
165         if (values[0].length() < 1) {
166           // Fall 1
167           range.setStart(0);
168           range.setEnd(Long.parseLong(values[1]));
169         } else {
170           // Fall 2
171           range.setStart(Long.parseLong(values[0]));
172           range.setEnd(Long.parseLong(values[1]));
173         }
174       }
175       ranges.addRange(range);
176     }
177     return ranges;
178   }
179
180   /**
181    * Einen Content-Range Header erzeugen
182    * 
183    * @param range die Range, aus deren Inhalt der Header erzeugt werden soll
184    * @param file  die Datei, die den Inhalt liefert, der vom Header 
185    * bezeichnet wird
186    * @return der Inhalt des Content-Range Headers
187    */
188   protected String contentRangeHdr(Range range, File file) {
189     StringBuilder sb = new StringBuilder();
190     sb.append(HttpResponder.STR_BYTES);
191     sb.append(FileServer.STR_BLANK);
192     sb.append(range.getStart());
193     sb.append(FileServer.STR_DASH);
194     sb.append(range.getEnd());
195     sb.append(FileServer.STR_SLASH);
196     sb.append(file.length());
197     return sb.toString();
198   }
199 }