/*
|
mini-server - Ein minimalistischer HTTP-Server
|
Copyright (C) 2021 Ulrich Hilger
|
|
This program is free software: you can redistribute it and/or modify
|
it under the terms of the GNU Affero General Public License as
|
published by the Free Software Foundation, either version 3 of the
|
License, or (at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
GNU Affero General Public License for more details.
|
|
You should have received a copy of the GNU Affero General Public License
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*/
|
package de.uhilger.minsrv.handler;
|
|
import com.sun.net.httpserver.Headers;
|
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpHandler;
|
import java.io.File;
|
import java.io.FileInputStream;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.OutputStream;
|
import java.nio.charset.StandardCharsets;
|
import java.text.SimpleDateFormat;
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.logging.Logger;
|
|
/**
|
* Die Klasse FileHandler dient zur Auslieferung von Dateiinhalten
|
* über HTTP.
|
*
|
* @author ulrich
|
* @version 0.1, 25. März 2021
|
*/
|
public class FileHandler implements HttpHandler {
|
|
private static final Logger logger = Logger.getLogger(FileHandler.class.getName());
|
|
final static String RANGE_HEADER = "Range";
|
final static String CONTENT_RANGE_HEADER = "Content-Range";
|
final static String ACCEPT_RANGES_HEADER = "Accept-Ranges";
|
final static String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
|
final static String CONTENT_TYPE = "Content-Type";
|
|
public static final int SC_PARTIAL_CONTENT = 206;
|
|
private final String basePath;
|
|
/**
|
* Ein neues Objekt der Klasse FileHandler erzeugen
|
*
|
* @param basePath der Pfad zu Inhalten, die von diesem Handler
|
* ausgeliefert werden
|
*/
|
public FileHandler(String basePath) {
|
this.basePath = basePath;
|
}
|
|
/**
|
* Die Datei ermitteln, die sich aus dem angefragten URL ergibt,
|
* prüfen, ob die Datei existiert und den Inhalt der Datei
|
* abhängig davon, ob ein Range-Header vorhanden ist,
|
* ganz oder teilweise ausliefern.
|
*
|
* @param e das Objekt mit Methoden zur Untersuchung
|
* der Anfrage sowie zum Anfertigen und Senden der Antwort
|
* @throws IOException falls etwas schief geht entsteht dieser Fehler
|
*/
|
@Override
|
public void handle(HttpExchange e) throws IOException {
|
String ctxPath = e.getHttpContext().getPath();
|
String uriPath = e.getRequestURI().getPath();
|
String fName = uriPath.substring(ctxPath.length());
|
if(fName.startsWith(".")) {
|
throw new IOException("Mit einem Punkt beginnende Dateinamen sind ungueltig.");
|
}
|
Headers headers = e.getRequestHeaders();
|
if (headers.containsKey(RANGE_HEADER)) {
|
logger.info("has range header");
|
File file = new File(basePath, fName);
|
logger.info(file.getAbsolutePath());
|
serveFileParts(e, file);
|
} else {
|
logger.info("no range header");
|
if (fName.endsWith("/")) {
|
fName += "index.html";
|
}
|
File file = new File(basePath, fName);
|
serveFile(e, file);
|
}
|
}
|
|
/**
|
* Den Inhalt einer Datei ausliefern
|
*
|
* @param e das Objekt mit Methoden zur Untersuchung
|
* der Anfrage sowie zum Anfertigen und Senden der Antwort
|
* @param file die Datei, deren Inhalt ausgeliefert werden soll
|
* @throws IOException falls etwas schief geht entsteht dieser Fehler
|
*/
|
private void serveFile(HttpExchange e, File file) throws IOException {
|
OutputStream os = e.getResponseBody();
|
if (file.exists()) {
|
e.sendResponseHeaders(200, file.length());
|
InputStream in = new FileInputStream(file);
|
int b = in.read();
|
while (b > -1) {
|
os.write(b);
|
b = in.read();
|
}
|
in.close();
|
} else {
|
String response = file.getName() + " not found.";
|
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
|
e.sendResponseHeaders(404, bytes.length);
|
os.write(bytes);
|
}
|
os.flush();
|
os.close();
|
}
|
|
/**
|
* Einen Teil des Inhalts einer Datei ausliefern
|
*
|
* @param e das Objekt mit Methoden zur Untersuchung
|
* der Anfrage sowie zum Anfertigen und Senden der Antwort
|
* @param file die Datei, deren Inhalt teilweise ausgeliefert werden soll
|
* @throws IOException falls etwas schief geht entsteht dieser Fehler
|
*/
|
/*
|
Wenn eine Range angefragt wird, hat die Antwort einen
|
Content-Range Header wie folgt:
|
|
Content-Range: bytes 0-1023/146515
|
Content-Length: 1024
|
|
Wenn mehrere Ranges angefragt werden, hat die Antwort mehrere
|
Content-Range Header als Multipart Response. Multipart Responses fehlen
|
dieser Implementierung noch.
|
|
(vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests)
|
*/
|
private void serveFileParts(HttpExchange e, File file) throws IOException {
|
InputStream is = new FileInputStream(file);
|
OutputStream os = e.getResponseBody();
|
Headers resHeaders = e.getResponseHeaders();
|
long responseLength = 0;
|
long start = 0;
|
long end;
|
RangeGroup rangeGroup = parseRanges(e, file);
|
Iterator<Range> i = rangeGroup.getRanges();
|
while(i.hasNext()) {
|
Range range = i.next();
|
start = range.getStart();
|
end = range.getEnd();
|
StringBuilder sb = new StringBuilder();
|
sb.append("bytes ");
|
sb.append(range.getStart());
|
sb.append("-");
|
sb.append(range.getEnd());
|
sb.append("/");
|
sb.append(file.length());
|
resHeaders.add(CONTENT_RANGE_HEADER, sb.toString());
|
logger.info(sb.toString());
|
responseLength += (end - start);
|
logger.info("responseLength: " + responseLength);
|
}
|
resHeaders.add(CONTENT_TYPE, "video/mp4");
|
SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
|
Date date = new Date(file.lastModified());
|
resHeaders.add(LAST_MODIFIED_DATE_HEADER, sdf.format(date));
|
e.sendResponseHeaders(SC_PARTIAL_CONTENT, responseLength);
|
if(start > 0) {
|
is.skip(start);
|
}
|
long count = 0;
|
int byteRead = is.read();
|
while(byteRead > -1 && count < responseLength) {
|
++count;
|
os.write(byteRead);
|
byteRead = is.read();
|
}
|
os.flush();
|
os.close();
|
is.close();
|
}
|
|
/**
|
* Die Byte-Ranges aus dem Range-Header ermitteln.
|
*
|
* Der Range-Header kann unterschiedliche Abschnitte bezeichnen, Beispiele:
|
* Range: bytes=200-1000, 2000-6576, 19000-
|
* Range: bytes=0-499, -500
|
* (vgl. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
|
*
|
* @param e das Objekt mit Methoden zur Untersuchung
|
* der Anfrage sowie zum Anfertigen und Senden der Antwort
|
* @param file die Datei, deren Inhalt ausgeliefert werden soll
|
* @return die angefragten Byte-Ranges
|
*/
|
private RangeGroup parseRanges(HttpExchange e, File file) {
|
RangeGroup ranges = new RangeGroup();
|
String rangeHeader = e.getRequestHeaders().get(RANGE_HEADER).toString();
|
|
/*
|
Inhalt des Range-Headers von nicht benoetigten Angaben befreien
|
|
Ein Range Header enthaelt neben den Start- und Endwerten der Ranges auch
|
die Angabe "bytes:". Es ist aber keine andere Auspraegung als Bytes
|
spezifiziert, daher muss die Angabe nicht ausgewertet werden und kann
|
entfallen. Der Range-Header kann zudem noch eckige Klammern haben
|
wie in [bytes=200-1000].
|
|
Der regulaere Ausdruck "[^\\d-,]" bezeichnet alle Zeichen, die keine
|
Ziffern 0-9, Bindestrich oder Komma sind.
|
*/
|
rangeHeader = rangeHeader.replaceAll("[^\\d-,]", "");
|
logger.info(rangeHeader);
|
|
/*
|
Die Ranges ermitteln.
|
|
Nach dem vorangegangenen Schritt besteht der Header-Ausdruck nur noch
|
aus einer mit Kommas getrennten Liste aus Start- und Endwerten wie z.B.
|
"-103,214-930,1647-"
|
|
Ein Range-Ausdruck kann dann drei verschiedene Auspraegungen haben:
|
1. Startwert fehlt, z.B. -200
|
2. Start und Ende sind vorhanden, z.B. 101-200
|
3. Endwert fehlt, z.B. 201-
|
|
Teilt man einen Range-String mit der Methode String.split("-") am
|
Bindestrich ('-') in ein String-Array 'values' gilt:
|
values.length < 2: Fall 3 ist gegeben
|
values.length > 1 und values[0].length < 1: Fall 1 ist gegeben
|
ansonsten: Fall 2 ist gegeben
|
*/
|
String[] rangeArray = rangeHeader.split(",");
|
for(String rangeStr : rangeArray) {
|
Range range = new Range();
|
String[] values = rangeStr.split("-");
|
if(values.length < 2) {
|
// Fall 3
|
range.setStart(Long.parseLong(values[0]));
|
range.setEnd(file.length());
|
} else {
|
if(values[0].length() < 1) {
|
// Fall 1
|
range.setStart(0);
|
range.setEnd(Long.parseLong(values[1]));
|
} else {
|
// Fall 2
|
range.setStart(Long.parseLong(values[0]));
|
range.setEnd(Long.parseLong(values[1]));
|
}
|
}
|
ranges.addRange(range);
|
}
|
return ranges;
|
}
|
|
/**
|
* Eine Range
|
*/
|
class Range {
|
private long start;
|
private long end;
|
|
/**
|
* Den Beginn dieser Range ermitteln
|
* @return Beginn dieser Range
|
*/
|
public long getStart() {
|
return start;
|
}
|
|
/**
|
* Den Beginn dieser Range angeben
|
* @param start Beginn dieser Range
|
*/
|
public void setStart(long start) {
|
this.start = start;
|
}
|
|
/**
|
* Das Ende dieser Range ermitteln
|
* @return Ende dieser Range
|
*/
|
public long getEnd() {
|
return end;
|
}
|
|
/**
|
* Das Ende dieser Range angeben
|
* @param end Ende dieser Range
|
*/
|
public void setEnd(long end) {
|
this.end = end;
|
}
|
}
|
|
/**
|
* Eine Gruppe aus Ranges
|
*/
|
class RangeGroup {
|
private List<Range> ranges;
|
private long totalSize;
|
|
/**
|
* Ein neues Objekt der Klasse RangeGroup erzeugen
|
*/
|
public RangeGroup() {
|
ranges = new ArrayList();
|
}
|
|
/**
|
* Dieser RangeGroup eine Range hinzufuegen.
|
*
|
* @param range die Range, die dieser RangeGroup hinzugefuegt werden soll
|
*/
|
public void addRange(Range range) {
|
ranges.add(range);
|
totalSize += range.getEnd() - range.getStart();
|
}
|
|
/**
|
* Die Gesamtgröße dieser RangeGroup ermitteln, also die
|
* Summe der Anzahl von Bytes aller ihrer Ranges.
|
*
|
* @return die Größe dieser RangeGroup in Bytes
|
*/
|
public long getSize() {
|
return totalSize;
|
}
|
|
/**
|
* Einen Iterator über die Ranges dieser RangeGroup abrufen
|
* @return Iterator über die Ranges dieser RangeGroup
|
*/
|
public Iterator<Range> getRanges() {
|
return ranges.iterator();
|
}
|
}
|
}
|