forked from j62/ctbrec
1
0
Fork 0

Add support for TLS and changing the context path

This commit is contained in:
0xboobface 2019-08-10 17:45:13 +02:00
parent bb02b5fd9f
commit f12a20a15e
14 changed files with 261 additions and 95 deletions

View File

@ -118,24 +118,28 @@ public class Player {
public void run() { public void run() {
running = true; running = true;
Runtime rt = Runtime.getRuntime(); Runtime rt = Runtime.getRuntime();
Config cfg = Config.getInstance();
try { try {
if (Config.getInstance().getSettings().localRecording && rec != null) { if (cfg.getSettings().localRecording && rec != null) {
File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath()); File file = new File(cfg.getSettings().recordingsDir, rec.getPath());
String[] args = new String[] { String[] args = new String[] {
Config.getInstance().getSettings().mediaPlayer, cfg.getSettings().mediaPlayer,
file.getName() file.getName()
}; };
playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile()); playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile());
} else { } else {
if(Config.getInstance().getSettings().requireAuthentication) { if(cfg.getSettings().requireAuthentication) {
URL u = new URL(url); URL u = new URL(url);
String path = u.getPath(); String path = u.getPath();
byte[] key = Config.getInstance().getSettings().key; if(!cfg.getContextPath().isEmpty()) {
path = path.substring(cfg.getContextPath().length());
}
byte[] key = cfg.getSettings().key;
String hmac = Hmac.calculate(path, key); String hmac = Hmac.calculate(path, key);
url = url + "?hmac=" + hmac; url = url + "?hmac=" + hmac;
} }
LOG.debug("Playing {}", url); LOG.debug("Playing {}", url);
playerProcess = rt.exec(Config.getInstance().getSettings().mediaPlayer + " " + url); playerProcess = rt.exec(cfg.getSettings().mediaPlayer + " " + url);
} }
// create threads, which read stdout and stderr of the player process. these are needed, // create threads, which read stdout and stderr of the player process. these are needed,

View File

@ -472,7 +472,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
File target = chooser.showSaveDialog(null); File target = chooser.showSaveDialog(null);
if(target != null) { if(target != null) {
config.getSettings().lastDownloadDir = target.getParent(); config.getSettings().lastDownloadDir = target.getParent();
String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls"; String hlsBase = config.getServerUrl() + "/hls";
URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8"); URL url = new URL(hlsBase + recording.getPath() + "/playlist.m3u8");
LOG.info("Downloading {}", recording.getPath()); LOG.info("Downloading {}", recording.getPath());
@ -542,7 +542,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} }
}.start(); }.start();
} else { } else {
String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls"; String hlsBase = Config.getInstance().getServerUrl() + "/hls";
url = hlsBase + recording.getPath() + "/playlist.m3u8"; url = hlsBase + recording.getPath() + "/playlist.m3u8";
new Thread() { new Thread() {
@Override @Override

View File

@ -61,8 +61,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private TextField overviewUpdateIntervalInSecs; private TextField overviewUpdateIntervalInSecs;
private TextField leaveSpaceOnDevice; private TextField leaveSpaceOnDevice;
private TextField minimumLengthInSecs; private TextField minimumLengthInSecs;
private TextField servletContext;
private CheckBox loadResolution; private CheckBox loadResolution;
private CheckBox secureCommunication = new CheckBox(); private CheckBox useAuthentication = new CheckBox();
private CheckBox useTLS = new CheckBox();
private CheckBox chooseStreamQuality = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox();
private CheckBox multiplePlayers = new CheckBox(); private CheckBox multiplePlayers = new CheckBox();
private CheckBox updateThumbnails = new CheckBox(); private CheckBox updateThumbnails = new CheckBox();
@ -152,7 +154,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private Node createRecordLocationPanel() { private Node createRecordLocationPanel() {
GridPane layout = createGridLayout(); GridPane layout = createGridLayout();
Label l = new Label("Record Location"); Label l = new Label("Record Location");
layout.add(l, 0, 0); int row = 0;
layout.add(l, 0, row);
recordLocation = new ToggleGroup(); recordLocation = new ToggleGroup();
recordLocal = new RadioButton("Local"); recordLocal = new RadioButton("Local");
recordRemote = new RadioButton("Remote"); recordRemote = new RadioButton("Remote");
@ -160,8 +163,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
recordRemote.setToggleGroup(recordLocation); recordRemote.setToggleGroup(recordLocation);
recordLocal.setSelected(Config.getInstance().getSettings().localRecording); recordLocal.setSelected(Config.getInstance().getSettings().localRecording);
recordRemote.setSelected(!recordLocal.isSelected()); recordRemote.setSelected(!recordLocal.isSelected());
layout.add(recordLocal, 1, 0); layout.add(recordLocal, 1, row);
layout.add(recordRemote, 2, 0); layout.add(recordRemote, 2, row++);
recordLocation.selectedToggleProperty().addListener((e) -> { recordLocation.selectedToggleProperty().addListener((e) -> {
Config.getInstance().getSettings().localRecording = recordLocal.isSelected(); Config.getInstance().getSettings().localRecording = recordLocal.isSelected();
setRecordingMode(recordLocal.isSelected()); setRecordingMode(recordLocal.isSelected());
@ -172,7 +175,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setMargin(recordLocal, new Insets(0, 0, CHECKBOX_MARGIN, 0)); GridPane.setMargin(recordLocal, new Insets(0, 0, CHECKBOX_MARGIN, 0));
GridPane.setMargin(recordRemote, new Insets(0, 0, CHECKBOX_MARGIN, 0)); GridPane.setMargin(recordRemote, new Insets(0, 0, CHECKBOX_MARGIN, 0));
layout.add(new Label("Server"), 0, 1); layout.add(new Label("Server"), 0, row);
server = new TextField(Config.getInstance().getSettings().httpServer); server = new TextField(Config.getInstance().getSettings().httpServer);
server.textProperty().addListener((ob, o, n) -> { server.textProperty().addListener((ob, o, n) -> {
if(!server.getText().isEmpty()) { if(!server.getText().isEmpty()) {
@ -183,9 +186,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setFillWidth(server, true); GridPane.setFillWidth(server, true);
GridPane.setHgrow(server, Priority.ALWAYS); GridPane.setHgrow(server, Priority.ALWAYS);
GridPane.setColumnSpan(server, 2); GridPane.setColumnSpan(server, 2);
layout.add(server, 1, 1); layout.add(server, 1, row++);
layout.add(new Label("Port"), 0, 2); layout.add(new Label("Port"), 0, row);
port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort)); port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort));
port.textProperty().addListener((observable, oldValue, newValue) -> { port.textProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue.matches("\\d*")) { if (!newValue.matches("\\d*")) {
@ -199,14 +202,27 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setFillWidth(port, true); GridPane.setFillWidth(port, true);
GridPane.setHgrow(port, Priority.ALWAYS); GridPane.setHgrow(port, Priority.ALWAYS);
GridPane.setColumnSpan(port, 2); GridPane.setColumnSpan(port, 2);
layout.add(port, 1, 2); layout.add(port, 1, row++);
layout.add(new Label("Path"), 0, row);
servletContext = new TextField(Config.getInstance().getSettings().servletContext);
servletContext.setPromptText("e.g. /ctbrec");
servletContext.setTooltip(new Tooltip("Leave empty, if you didn't change the servletContext in the server config"));
servletContext.textProperty().addListener((observable, oldValue, newValue) -> {
Config.getInstance().getSettings().servletContext = servletContext.getText();
saveConfig();
});
GridPane.setFillWidth(servletContext, true);
GridPane.setHgrow(servletContext, Priority.ALWAYS);
GridPane.setColumnSpan(servletContext, 2);
layout.add(servletContext, 1, row++);
l = new Label("Require authentication"); l = new Label("Require authentication");
layout.add(l, 0, 3); layout.add(l, 0, row);
secureCommunication.setSelected(Config.getInstance().getSettings().requireAuthentication); useAuthentication.setSelected(Config.getInstance().getSettings().requireAuthentication);
secureCommunication.setOnAction((e) -> { useAuthentication.setOnAction((e) -> {
Config.getInstance().getSettings().requireAuthentication = secureCommunication.isSelected(); Config.getInstance().getSettings().requireAuthentication = useAuthentication.isSelected();
if(secureCommunication.isSelected()) { if(useAuthentication.isSelected()) {
byte[] key = Config.getInstance().getSettings().key; byte[] key = Config.getInstance().getSettings().key;
if(key == null) { if(key == null) {
key = Hmac.generateKey(); key = Hmac.generateKey();
@ -226,8 +242,19 @@ public class SettingsTab extends Tab implements TabSelectionListener {
} }
}); });
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0)); GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0)); GridPane.setMargin(useAuthentication, new Insets(4, 0, 0, 0));
layout.add(secureCommunication, 1, 3); layout.add(useAuthentication, 1, row++);
l = new Label("Use Secure Communication (TLS)");
layout.add(l, 0, row);
useTLS.setSelected(Config.getInstance().getSettings().transportLayerSecurity);
useTLS.setOnAction((e) -> {
Config.getInstance().getSettings().transportLayerSecurity = useTLS.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
GridPane.setMargin(useTLS, new Insets(4, 0, 0, 0));
layout.add(useTLS, 1, row++);
TitledPane recordLocation = new TitledPane("Record Location", layout); TitledPane recordLocation = new TitledPane("Record Location", layout);
recordLocation.setCollapsible(false); recordLocation.setCollapsible(false);
@ -582,7 +609,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private void setRecordingMode(boolean local) { private void setRecordingMode(boolean local) {
server.setDisable(local); server.setDisable(local);
port.setDisable(local); port.setDisable(local);
secureCommunication.setDisable(local); useAuthentication.setDisable(local);
useTLS.setDisable(local);
recordingsDirectory.setDisable(!local); recordingsDirectory.setDisable(!local);
splitAfter.setDisable(!local); splitAfter.setDisable(!local);
maxResolution.setDisable(!local); maxResolution.setDisable(!local);

View File

@ -11,6 +11,7 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -163,4 +164,23 @@ public class Config {
return new File(getSettings().recordingsDir); return new File(getSettings().recordingsDir);
} }
} }
public String getServerUrl() {
String scheme = getSettings().transportLayerSecurity ? "https" : "http";
//int port = getSettings().transportLayerSecurity ? getSettings().httpSecurePort : getSettings().httpPort;
int port = getSettings().httpPort;
String baseUrl = scheme + "://" + getSettings().httpServer + ":" + port + getContextPath();
return baseUrl;
}
public String getContextPath() {
String context = Optional.ofNullable(getSettings().servletContext).orElse("");
if (!context.startsWith("/") && !context.isEmpty()) {
context = '/' + context;
}
if (context.endsWith("/")) {
context = context.substring(0, context.length() - 1);
}
return context;
}
} }

View File

@ -54,6 +54,7 @@ public class Settings {
public String flirt4freeUsername; public String flirt4freeUsername;
public boolean generatePlaylist = true; public boolean generatePlaylist = true;
public int httpPort = 8080; public int httpPort = 8080;
public int httpSecurePort = 8443;
public String httpServer = "localhost"; public String httpServer = "localhost";
public int httpTimeout = 10000; public int httpTimeout = 10000;
public String httpUserAgent = "Mozilla/5.0 Gecko/20100101 Firefox/62.0"; public String httpUserAgent = "Mozilla/5.0 Gecko/20100101 Firefox/62.0";
@ -100,12 +101,14 @@ public class Settings {
public String recordingsSortColumn = ""; public String recordingsSortColumn = "";
public String recordingsSortType = ""; public String recordingsSortType = "";
public boolean requireAuthentication = false; public boolean requireAuthentication = false;
public String servletContext = "";
public boolean showPlayerStarting = false; public boolean showPlayerStarting = false;
public boolean singlePlayer = true; public boolean singlePlayer = true;
public int splitRecordings = 0; public int splitRecordings = 0;
public String startTab = "Settings"; public String startTab = "Settings";
public String streamatePassword = ""; public String streamatePassword = "";
public String streamateUsername = ""; public String streamateUsername = "";
public boolean transportLayerSecurity = true;
public int thumbWidth = 180; public int thumbWidth = 180;
public boolean updateThumbnails = true; public boolean updateThumbnails = true;
public String username = ""; // chaturbate username TODO maybe rename this onetime public String username = ""; // chaturbate username TODO maybe rename this onetime

View File

@ -6,6 +6,11 @@ import java.io.IOException;
import java.net.Authenticator; import java.net.Authenticator;
import java.net.PasswordAuthentication; import java.net.PasswordAuthentication;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -13,6 +18,12 @@ import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -124,9 +135,45 @@ public abstract class HttpClient {
} }
} }
// if transport layer security (TLS) is switched on, accept the self signed cert from the server
if (Config.getInstance().getSettings().transportLayerSecurity) {
acceptAllTlsCerts(builder);
}
client = builder.build(); client = builder.build();
} }
/**
* This is a very simple and insecure solution to accept the self-signed cert from
* the server. The side effect is, that certificates from other servers are neither checked!
* TODO Delegate to the default trustmanager, if it is not the self-signed cert
*/
private void acceptAllTlsCerts(Builder builder) {
X509TrustManager x509TrustManager = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
X509Certificate[] x509Certificates = new X509Certificate[0];
return x509Certificates;
}
@Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {}
@Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {}
};
try {
final TrustManager[] trustManagers = new TrustManager[] { x509TrustManager };
final String PROTOCOL = "TLSv1.2";
SSLContext sslContext = SSLContext.getInstance(PROTOCOL);
KeyManager[] keyManagers = null;
SecureRandom secureRandom = new SecureRandom();
sslContext.init(keyManagers, trustManagers, secureRandom);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
builder.sslSocketFactory(sslSocketFactory, x509TrustManager);
builder.hostnameVerifier((name, sslSession) -> true);
} catch (KeyManagementException | NoSuchAlgorithmException e) {
LOG.error("Couldn't install trust managers for TLS connections");
}
}
public void shutdown() { public void shutdown() {
persistCookies(); persistCookies();
client.connectionPool().evictAll(); client.connectionPool().evictAll();

View File

@ -38,6 +38,7 @@ import okhttp3.Response;
public class RemoteRecorder implements Recorder { public class RemoteRecorder implements Recorder {
private static final transient Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class); private static final transient Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class);
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private Moshi moshi = new Moshi.Builder() private Moshi moshi = new Moshi.Builder()
.add(Instant.class, new InstantJsonAdapter()) .add(Instant.class, new InstantJsonAdapter())
@ -71,6 +72,10 @@ public class RemoteRecorder implements Recorder {
syncThread.start(); syncThread.start();
} }
private String getRecordingEndpoint() {
return config.getServerUrl() + "/rec";
}
@Override @Override
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
sendRequest("start", model); sendRequest("start", model);
@ -86,7 +91,7 @@ public class RemoteRecorder implements Recorder {
LOG.debug("Sending request to recording server: {}", payload); LOG.debug("Sending request to recording server: {}", payload);
RequestBody body = RequestBody.create(JSON, payload); RequestBody body = RequestBody.create(JSON, payload);
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(payload, builder); addHmacIfNeeded(payload, builder);
Request request = builder.build(); Request request = builder.build();
@ -172,7 +177,7 @@ public class RemoteRecorder implements Recorder {
String msg = "{\"action\": \"space\"}"; String msg = "{\"action\": \"space\"}";
RequestBody body = RequestBody.create(JSON, msg); RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();
@ -196,7 +201,7 @@ public class RemoteRecorder implements Recorder {
String msg = "{\"action\": \"list\"}"; String msg = "{\"action\": \"list\"}";
RequestBody body = RequestBody.create(JSON, msg); RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();
@ -231,7 +236,7 @@ public class RemoteRecorder implements Recorder {
String msg = "{\"action\": \"listOnline\"}"; String msg = "{\"action\": \"listOnline\"}";
RequestBody body = RequestBody.create(JSON, msg); RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();
@ -265,7 +270,7 @@ public class RemoteRecorder implements Recorder {
String msg = "{\"action\": \"recordings\"}"; String msg = "{\"action\": \"recordings\"}";
RequestBody body = RequestBody.create(JSON, msg); RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();
@ -344,7 +349,7 @@ public class RemoteRecorder implements Recorder {
String msg = recordingRequestAdapter.toJson(recReq); String msg = recordingRequestAdapter.toJson(recReq);
RequestBody body = RequestBody.create(JSON, msg); RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();
@ -472,7 +477,8 @@ public class RemoteRecorder implements Recorder {
String msg = recordingRequestAdapter.toJson(recReq); String msg = recordingRequestAdapter.toJson(recReq);
LOG.debug(msg); LOG.debug(msg);
RequestBody body = RequestBody.create(JSON, msg); RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder().url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") Request.Builder builder = new Request.Builder()
.url(getRecordingEndpoint())
.post(body); .post(body);
addHmacIfNeeded(msg, builder); addHmacIfNeeded(msg, builder);
Request request = builder.build(); Request request = builder.build();

View File

@ -89,6 +89,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
URL u = new URL(segmentPlaylistUri); URL u = new URL(segmentPlaylistUri);
String path = u.getPath(); String path = u.getPath();
byte[] key = Config.getInstance().getSettings().key; byte[] key = Config.getInstance().getSettings().key;
if(!Config.getInstance().getContextPath().isEmpty()) {
path = path.substring(Config.getInstance().getContextPath().length());
}
String hmac = Hmac.calculate(path, key); String hmac = Hmac.calculate(path, key);
segmentPlaylistUri = segmentPlaylistUri + "?hmac=" + hmac; segmentPlaylistUri = segmentPlaylistUri + "?hmac=" + hmac;
} }

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -13,14 +15,19 @@ import javax.servlet.http.HttpServletResponse;
public class StaticFileServlet extends HttpServlet { public class StaticFileServlet extends HttpServlet {
private String classPathRoot; private String classPathRoot;
private Map<String, String> mimetypes = new HashMap<>();
public StaticFileServlet(String classPathRoot) { public StaticFileServlet(String classPathRoot) {
this.classPathRoot = classPathRoot; this.classPathRoot = classPathRoot;
mimetypes.put("css", "text/css");
mimetypes.put("js", "application/javascript");
} }
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String request = req.getRequestURI(); String request = req.getRequestURI();
String contextPath = getServletContext().getContextPath();
request = request.substring(contextPath.length());
serveFile(request, resp); serveFile(request, resp);
} }
@ -34,7 +41,12 @@ public class StaticFileServlet extends HttpServlet {
if (resourceAsStream == null) { if (resourceAsStream == null) {
throw new FileNotFoundException(); throw new FileNotFoundException();
} }
resp.setContentType(URLConnection.guessContentTypeFromName(resource));
String mimetype = URLConnection.guessContentTypeFromName(resource);
if (mimetype == null) {
mimetype = guessMimeType(resource);
}
resp.setContentType(mimetype);
resp.setStatus(HttpServletResponse.SC_OK); resp.setStatus(HttpServletResponse.SC_OK);
OutputStream out = resp.getOutputStream(); OutputStream out = resp.getOutputStream();
int length = 0; int length = 0;
@ -43,4 +55,12 @@ public class StaticFileServlet extends HttpServlet {
out.write(buffer, 0, length); out.write(buffer, 0, length);
} }
} }
private String guessMimeType(String resource) {
try {
String extension = resource.substring(resource.lastIndexOf('.') + 1);
return mimetypes.get(extension);
} catch(Exception e) {}
return null;
}
} }

View File

@ -20,6 +20,7 @@ public abstract class AbstractCtbrecServlet extends HttpServlet {
String httpHeaderHmac = req.getHeader("CTBREC-HMAC"); String httpHeaderHmac = req.getHeader("CTBREC-HMAC");
String hmac = null; String hmac = null;
String url = req.getRequestURI(); String url = req.getRequestURI();
url = url.substring(getServletContext().getContextPath().length());
if(reqParamHmac != null) { if(reqParamHmac != null) {
hmac = reqParamHmac; hmac = reqParamHmac;

View File

@ -32,7 +32,8 @@ public class HlsServlet extends AbstractCtbrecServlet {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String request = req.getRequestURI().substring(5); String contextPath = getServletContext().getContextPath();
String request = req.getRequestURI().substring(contextPath.length() + 5);
File recordingsDir = new File(config.getSettings().recordingsDir); File recordingsDir = new File(config.getSettings().recordingsDir);
File requestedFile = new File(recordingsDir, request); File requestedFile = new File(recordingsDir, request);

View File

@ -5,8 +5,10 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.BindException; import java.net.BindException;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@ -25,11 +27,12 @@ import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.SecuredRedirectHandler;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -140,48 +143,62 @@ public class HttpServer {
HttpConfiguration config = new HttpConfiguration(); HttpConfiguration config = new HttpConfiguration();
config.setSendServerVersion(false); config.setSendServerVersion(false);
ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(config)); config.setSecurePort(this.config.getSettings().httpSecurePort);
config.setSecureScheme("https");
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(config);
SslContextFactory sslContextFactory = new SslContextFactory.Server();
URL keyStoreUrl = getClass().getResource("/keystore.pkcs12");
String keyStoreSrc = System.getProperty("keystore.file", keyStoreUrl.toExternalForm());
String keyStorePassword = System.getProperty("keystore.password", "ctbrecsucks");
sslContextFactory.setKeyStorePath(keyStoreSrc);
sslContextFactory.setKeyStorePassword(keyStorePassword);
sslContextFactory.setTrustStorePath(keyStoreSrc);
sslContextFactory.setTrustStorePassword(keyStorePassword);
// connector for http
ServerConnector http = new ServerConnector(server, httpConnectionFactory);
http.setPort(this.config.getSettings().httpPort); http.setPort(this.config.getSettings().httpPort);
http.setIdleTimeout(this.config.getSettings().httpTimeout); http.setIdleTimeout(this.config.getSettings().httpTimeout);
server.addConnector(http);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); // connector for https (TLS)
context.setContextPath("/secured/*"); ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory);
server.setHandler(context); https.setPort(this.config.getSettings().httpSecurePort);
https.setIdleTimeout(this.config.getSettings().httpTimeout);
ServletHandler handler = new ServletHandler(); String contextPath = Config.getInstance().getContextPath();
//server.setHandler(handler); ServletContextHandler basicAuthContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
HandlerList handlers = new HandlerList(); basicAuthContext.setContextPath(contextPath + "/secured");
handlers.setHandlers(new Handler[] { context, handler });
server.setHandler(handlers); ServletContextHandler defaultContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
defaultContext.setContextPath(contextPath);
RecorderServlet recorderServlet = new RecorderServlet(recorder, sites); RecorderServlet recorderServlet = new RecorderServlet(recorder, sites);
ServletHolder holder = new ServletHolder(recorderServlet); ServletHolder holder = new ServletHolder(recorderServlet);
handler.addServletWithMapping(holder, "/rec"); defaultContext.addServlet(holder, "/rec");
HlsServlet hlsServlet = new HlsServlet(this.config); HlsServlet hlsServlet = new HlsServlet(this.config);
holder = new ServletHolder(hlsServlet); holder = new ServletHolder(hlsServlet);
handler.addServletWithMapping(holder, "/hls/*"); defaultContext.addServlet(holder, "/hls/*");
if (this.config.getSettings().webinterface) { if (this.config.getSettings().webinterface) {
LOG.info("Register static file servlet under {}", context.getContextPath());
StaticFileServlet staticFileServlet = new StaticFileServlet("/html"); StaticFileServlet staticFileServlet = new StaticFileServlet("/html");
holder = new ServletHolder(staticFileServlet); holder = new ServletHolder(staticFileServlet);
handler.addServletWithMapping(holder, "/static/*"); String staticFileContext = "/static/*";
//context.addServlet(holder, "/"); defaultContext.addServlet(holder, staticFileContext);
LOG.info("Register static file servlet under {}", defaultContext.getContextPath()+staticFileContext);
// servlet to retrieve the HMAC secured by basic auth // servlet to retrieve the HMAC secured by basic auth
String username = this.config.getSettings().webinterfaceUsername; String username = this.config.getSettings().webinterfaceUsername;
String password = this.config.getSettings().webinterfacePassword; String password = this.config.getSettings().webinterfacePassword;
context.setSecurityHandler(basicAuth(username, password, "CTB Recorder")); basicAuthContext.setSecurityHandler(basicAuth(username, password, "CTB Recorder"));
context.addServlet(new ServletHolder(new HttpServlet() { basicAuthContext.addServlet(new ServletHolder(new HttpServlet() {
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (Objects.equal(username, req.getRemoteUser())) { if (Objects.equal(username, req.getRemoteUser())) {
resp.setStatus(HttpServletResponse.SC_OK); resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("application/json"); resp.setContentType("application/json");
byte[] hmac = HttpServer.this.config.getSettings().key; byte[] hmac = Optional.ofNullable(HttpServer.this.config.getSettings().key).orElse(new byte[0]);
JSONObject response = new JSONObject(); JSONObject response = new JSONObject();
response.put("hmac", new String(hmac, "utf-8")); response.put("hmac", new String(hmac, "utf-8"));
resp.getOutputStream().println(response.toString()); resp.getOutputStream().println(response.toString());
@ -190,12 +207,25 @@ public class HttpServer {
}), "/hmac"); }), "/hmac");
} }
server.addConnector(http);
HandlerList handlers = new HandlerList();
if (this.config.getSettings().transportLayerSecurity) {
server.addConnector(https);
handlers.setHandlers(new Handler[] { new SecuredRedirectHandler(), basicAuthContext, defaultContext });
} else {
handlers.setHandlers(new Handler[] { basicAuthContext, defaultContext });
}
server.setHandler(handlers);
try { try {
server.start(); server.start();
server.join(); server.join();
} catch (BindException e) { } catch (BindException e) {
LOG.error("Port {} is already in use", http.getPort(), e); LOG.error("Port {} is already in use", http.getPort(), e);
System.exit(1); System.exit(1);
} catch (Exception e) {
LOG.error("Server start failed", e);
System.exit(1);
} }
} }
@ -238,7 +268,8 @@ public class HttpServer {
LOG.debug("Java:\t{} {} {}", System.getProperty("java.vendor"), System.getProperty("java.vm.name"), System.getProperty("java.version")); LOG.debug("Java:\t{} {} {}", System.getProperty("java.vendor"), System.getProperty("java.vm.name"), System.getProperty("java.version"));
try { try {
LOG.debug("ctbrec server {}", getVersion().toString()); LOG.debug("ctbrec server {}", getVersion().toString());
} catch (IOException e) {} } catch (IOException e) {
}
} }
private Version getVersion() throws IOException { private Version getVersion() throws IOException {

View File

@ -11,27 +11,27 @@
<title>CTB Recorder</title> <title>CTB Recorder</title>
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link href="/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom fonts for this template --> <!-- Custom fonts for this template -->
<link href="/static/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css"> <link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic" rel="stylesheet" type="text/css"> <link href="https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic" rel="stylesheet" type="text/css">
<!-- Plugin CSS --> <!-- Plugin CSS -->
<link href="/static/vendor/magnific-popup/magnific-popup.css" rel="stylesheet" type="text/css"> <link href="vendor/magnific-popup/magnific-popup.css" rel="stylesheet" type="text/css">
<link href="/static/vendor/jquery-ui/jquery-ui-1.12.1.css" rel="stylesheet" type="text/css"> <link href="vendor/jquery-ui/jquery-ui-1.12.1.css" rel="stylesheet" type="text/css">
<!-- Custom styles for this template --> <!-- Custom styles for this template -->
<link href="/static/freelancer.css" rel="stylesheet"> <link href="freelancer.css" rel="stylesheet">
<!-- Flowplayer --> <!-- Flowplayer -->
<link rel="stylesheet" href="/static/vendor/flowplayer/skin/skin.css"> <link rel="stylesheet" href="vendor/flowplayer/skin/skin.css">
<!-- custom css --> <!-- custom css -->
<link rel="stylesheet" href="/static/custom.css"> <link rel="stylesheet" href="custom.css">
<link rel="shortcut icon" href="/static/favicon.png" type="image/x-icon" /> <link rel="shortcut icon" href="favicon.png" type="image/x-icon" />
<style> <style>
.ui-front { .ui-front {
@ -45,7 +45,7 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-expand-lg bg-secondary fixed-top text-uppercase" id="mainNav" style="padding-pottom: 3rem"> <nav class="navbar navbar-expand-lg bg-secondary fixed-top text-uppercase" id="mainNav" style="padding-pottom: 3rem">
<div class="container"> <div class="container">
<a class="navbar-brand js-scroll-trigger" href="/static/index.html"><img src="/static/icon64.png" alt="Logo" />CTBREC</a> <a class="navbar-brand js-scroll-trigger" href="index.html"><img src="icon64.png" alt="Logo" />CTBREC</a>
<button class="navbar-toggler navbar-toggler-right text-uppercase bg-primary text-white rounded" type="button" data-toggle="collapse" <button class="navbar-toggler navbar-toggler-right text-uppercase bg-primary text-white rounded" type="button" data-toggle="collapse"
data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
Menu <i class="fa fa-bars"></i> Menu <i class="fa fa-bars"></i>
@ -183,22 +183,22 @@
</div> </div>
<!-- Bootstrap core JavaScript --> <!-- Bootstrap core JavaScript -->
<script src="/static/vendor/jquery/jquery.min.js"></script> <script src="vendor/jquery/jquery.min.js"></script>
<script src="/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script> <script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
<!-- Plugin JavaScript --> <!-- Plugin JavaScript -->
<script src="/static/vendor/jquery-ui/jquery-ui-1.12.1.js"></script> <script src="vendor/jquery-ui/jquery-ui-1.12.1.js"></script>
<script src="/static/vendor/jquery-easing/jquery.easing.min.js"></script> <script src="vendor/jquery-easing/jquery.easing.min.js"></script>
<script src="/static/vendor/magnific-popup/jquery.magnific-popup.min.js"></script> <script src="vendor/magnific-popup/jquery.magnific-popup.min.js"></script>
<script src="/static/vendor/notify.js/notify.min.js"></script> <script src="vendor/notify.js/notify.min.js"></script>
<!-- knockout --> <!-- knockout -->
<script src="/static/vendor/knockout/knockout-3.5.0.js"></script> <script src="vendor/knockout/knockout-3.5.0.js"></script>
<script src="/static/vendor/knockout-orderable/knockout.bindings.orderable.js"></script> <script src="vendor/knockout-orderable/knockout.bindings.orderable.js"></script>
<!-- Custom scripts for this template --> <!-- Custom scripts for this template -->
<script src="/static/freelancer.min.js"></script> <script src="freelancer.min.js"></script>
<div id="player-window" class="modal"> <div id="player-window" class="modal">
<div class="modal-content"> <div class="modal-content">
@ -209,10 +209,10 @@
</div> </div>
<!-- HLS MediaSource support --> <!-- HLS MediaSource support -->
<script src="/static/vendor/hls.js/hls.js"></script> <script src="vendor/hls.js/hls.js"></script>
<!-- CryptoJS for HMAc authentication --> <!-- CryptoJS for HMAc authentication -->
<script src="/static/vendor/CryptoJS/hmac-sha256.js"></script> <script src="vendor/CryptoJS/hmac-sha256.js"></script>
<script> <script>
let onlineModels = []; let onlineModels = [];
@ -248,7 +248,7 @@
let action = '{"action": "startByUrl", "model": ' + JSON.stringify(model) + '}'; let action = '{"action": "startByUrl", "model": ' + JSON.stringify(model) + '}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -277,7 +277,7 @@
let action = '{"action": "resume", "model": ' + JSON.stringify(model) + '}'; let action = '{"action": "resume", "model": ' + JSON.stringify(model) + '}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -305,7 +305,7 @@
let action = '{"action": "suspend", "model": ' + JSON.stringify(model) + '}'; let action = '{"action": "suspend", "model": ' + JSON.stringify(model) + '}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -333,7 +333,7 @@
let action = '{"action": "stop", "model": ' + JSON.stringify(model) + '}'; let action = '{"action": "stop", "model": ' + JSON.stringify(model) + '}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -363,7 +363,7 @@
let action = '{"action": "delete", "recording": ' + JSON.stringify(recording) + '}'; let action = '{"action": "delete", "recording": ' + JSON.stringify(recording) + '}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -395,9 +395,12 @@
hls.attachMedia(video); hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () { hls.on(Hls.Events.MEDIA_ATTACHED, function () {
let src = recording.playlist; let src = recording.playlist;
let hmacOfPath = CryptoJS.HmacSHA256(src, hmac);
src = '..' + src;
if (hmac.length > 0) { if (hmac.length > 0) {
src += "?hmac=" + CryptoJS.HmacSHA256(src, hmac); src += "?hmac=" + hmacOfPath;
} }
console.log(src);
hls.loadSource(src); hls.loadSource(src);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) { hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
console.log("manifest loaded, found " + data.levels.length + " quality level"); console.log("manifest loaded, found " + data.levels.length + " quality level");
@ -432,19 +435,18 @@
$(document).ready(function() { $(document).ready(function() {
if (localStorage !== undefined && localStorage.hmac !== undefined) { if (localStorage !== undefined && localStorage.hmac !== undefined) {
console.log('using hmac from cookie'); console.log('using hmac from local storage');
hmac = localStorage.hmac; hmac = localStorage.hmac;
} else { } else {
console.log('hmac not found in local storage. requesting hmac from server'); console.log('hmac not found in local storage. requesting hmac from server');
$.ajax({ $.ajax({
type : 'GET', type : 'GET',
url : '/secured/hmac', url : '../secured/hmac',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000 timeout : 60000
}) })
.done(function(data) { .done(function(data) {
console.log(data);
hmac = data.hmac; hmac = data.hmac;
if (localStorage !== undefined) { if (localStorage !== undefined) {
console.log('saving hmac to local storage'); console.log('saving hmac to local storage');
@ -479,7 +481,7 @@
let action = '{"action": "listOnline"}'; let action = '{"action": "listOnline"}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -590,7 +592,7 @@
let action = '{"action": "list"}'; let action = '{"action": "list"}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -682,7 +684,7 @@
let action = '{"action": "recordings"}'; let action = '{"action": "recordings"}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -710,7 +712,7 @@
let action = '{"action": "space"}'; let action = '{"action": "space"}';
$.ajax({ $.ajax({
type : 'POST', type : 'POST',
url : '/rec', url : '../rec',
dataType : 'json', dataType : 'json',
async : true, async : true,
timeout : 60000, timeout : 60000,
@ -736,7 +738,7 @@
updateRecordings(); updateRecordings();
}); });
</script> </script>
<script src="/static/modal.js"></script> <script src="modal.js"></script>
</body> </body>
</html> </html>

Binary file not shown.