Add byte range support for HTTP requests

This commit is contained in:
0xboobface 2019-12-08 18:37:26 +01:00
parent cb90ff8264
commit 5ce2bd9901
1 changed files with 66 additions and 30 deletions

View File

@ -2,11 +2,13 @@ package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*; import static javax.servlet.http.HttpServletResponse.*;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -15,16 +17,13 @@ import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
public class HlsServlet extends AbstractCtbrecServlet { public class HlsServlet extends AbstractCtbrecServlet {
private static final transient Logger LOG = LoggerFactory.getLogger(HlsServlet.class); private static final transient Logger LOG = LoggerFactory.getLogger(HlsServlet.class);
private Config config; private final Config config;
public HlsServlet(Config config) { public HlsServlet(Config config) {
this.config = config; this.config = config;
@ -42,34 +41,38 @@ public class HlsServlet extends AbstractCtbrecServlet {
try { try {
boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI()); boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI());
if (!isRequestAuthenticated) { if (!isRequestAuthenticated) {
resp.setStatus(SC_UNAUTHORIZED); writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}");
String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}";
resp.getWriter().write(response);
return; return;
} }
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
resp.setStatus(SC_UNAUTHORIZED); writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}");
String response = "{\"status\": \"error\", \"msg\": \"Authentication failed\"}";
resp.getWriter().write(response);
return; return;
} }
try {
servePlaylist(req, resp, requestedFile); servePlaylist(req, resp, requestedFile);
} catch (ParseException | PlaylistException e) {
LOG.error("Error while generating playlist file", e);
throw new IOException("Couldn't generate playlist file " + requestedFile, e);
}
} else { } else {
if (requestedFile.exists()) { if (requestedFile.exists()) {
Enumeration<String> headerNames = req.getHeaderNames();
while(headerNames.hasMoreElements()) {
String header = headerNames.nextElement();
LOG.info("{}: {}", header, req.getHeader(header));
}
serveSegment(req, resp, requestedFile); serveSegment(req, resp, requestedFile);
} else { } else {
error404(req, resp); error404(req, resp);
} }
} }
} else { } else {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN); writeResponse(resp, SC_FORBIDDEN, "Stop it!");
resp.getWriter().println("Stop it!"); }
}
private void writeResponse(HttpServletResponse resp, int code, String body) {
try {
resp.setStatus(code);
resp.getWriter().write(body);
} catch (IOException e) {
LOG.error("Couldn't write HTTP response", e);
} }
} }
@ -77,23 +80,32 @@ public class HlsServlet extends AbstractCtbrecServlet {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
} }
private void serveSegment(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws FileNotFoundException, IOException { private void serveSegment(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException {
serveFile(resp, requestedFile, "application/octet-stream"); String mimetype = requestedFile.getName().endsWith(".mp4") ? "video/mp4" : "application/octet-stream";
serveFile(req, resp, requestedFile, mimetype);
} }
private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws FileNotFoundException, IOException, ParseException, PlaylistException { private void servePlaylist(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException {
serveFile(resp, requestedFile, "application/x-mpegURL"); serveFile(req, resp, requestedFile, "application/x-mpegURL");
} }
private void serveFile(HttpServletResponse resp, File file, String contentType) throws FileNotFoundException, IOException { private void serveFile(HttpServletRequest req, HttpServletResponse resp, File file, String contentType) throws IOException {
LOG.trace("Serving segment {}", file.getAbsolutePath()); ByteRange range = getByteRange(req, file);
resp.setStatus(200); LOG.info("Serving segment {} range: {}-{}", file.getAbsolutePath(), range.from, range.to);
resp.setContentLength((int) file.length()); resp.setStatus(range.set ? SC_PARTIAL_CONTENT : SC_OK);
resp.setContentType(contentType); resp.setContentType(contentType);
try(FileInputStream fin = new FileInputStream(file)) { try (RandomAccessFile fin = new RandomAccessFile(file, "r")) {
fin.seek(range.from);
byte[] buffer = new byte[1024 * 100]; byte[] buffer = new byte[1024 * 100];
long bytesLeft = range.to - range.from;
resp.setContentLengthLong(bytesLeft);
if (range.set) {
resp.setHeader("Content-Range", "bytes " + range.from + '-' + range.to + '/' + file.length());
}
int bytesToRead = (int) Math.min(bytesLeft, buffer.length);
int length = -1; int length = -1;
while( (length = fin.read(buffer)) >= 0) { while ((length = fin.read(buffer, 0, bytesToRead)) >= 0) {
bytesLeft -= length;
resp.getOutputStream().write(buffer, 0, length); resp.getOutputStream().write(buffer, 0, length);
} }
} }
@ -103,4 +115,28 @@ public class HlsServlet extends AbstractCtbrecServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp); doGet(req, resp);
} }
private ByteRange getByteRange(HttpServletRequest req, File file) {
ByteRange range = new ByteRange();
range.to = file.length();
String rangeHeader = req.getHeader("Range");
if (rangeHeader != null) {
range.set = true;
Matcher m = Pattern.compile("bytes=(\\d+)-(\\d+)*").matcher(rangeHeader);
if (m.find()) {
range.from = Long.parseLong(m.group(1));
String to = m.group(2);
if (to != null && !to.trim().isEmpty()) {
range.to = Long.parseLong(to);
}
}
}
return range;
}
class ByteRange {
boolean set = false;
long from = 0;
long to = Long.MAX_VALUE;
}
} }