diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index afb6b1cb..b31878af 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -118,24 +118,28 @@ public class Player { public void run() { running = true; Runtime rt = Runtime.getRuntime(); + Config cfg = Config.getInstance(); try { - if (Config.getInstance().getSettings().localRecording && rec != null) { - File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath()); + if (cfg.getSettings().localRecording && rec != null) { + File file = new File(cfg.getSettings().recordingsDir, rec.getPath()); String[] args = new String[] { - Config.getInstance().getSettings().mediaPlayer, + cfg.getSettings().mediaPlayer, file.getName() }; playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile()); } else { - if(Config.getInstance().getSettings().requireAuthentication) { + if(cfg.getSettings().requireAuthentication) { URL u = new URL(url); 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); url = url + "?hmac=" + hmac; } 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, diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 4dabbd9a..46767088 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -472,7 +472,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { File target = chooser.showSaveDialog(null); if(target != null) { 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"); LOG.info("Downloading {}", recording.getPath()); @@ -542,7 +542,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { } }.start(); } else { - String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls"; + String hlsBase = Config.getInstance().getServerUrl() + "/hls"; url = hlsBase + recording.getPath() + "/playlist.m3u8"; new Thread() { @Override diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index aa60cbaa..61059384 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -61,8 +61,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { private TextField overviewUpdateIntervalInSecs; private TextField leaveSpaceOnDevice; private TextField minimumLengthInSecs; + private TextField servletContext; 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 multiplePlayers = new CheckBox(); private CheckBox updateThumbnails = new CheckBox(); @@ -152,7 +154,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { private Node createRecordLocationPanel() { GridPane layout = createGridLayout(); Label l = new Label("Record Location"); - layout.add(l, 0, 0); + int row = 0; + layout.add(l, 0, row); recordLocation = new ToggleGroup(); recordLocal = new RadioButton("Local"); recordRemote = new RadioButton("Remote"); @@ -160,8 +163,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { recordRemote.setToggleGroup(recordLocation); recordLocal.setSelected(Config.getInstance().getSettings().localRecording); recordRemote.setSelected(!recordLocal.isSelected()); - layout.add(recordLocal, 1, 0); - layout.add(recordRemote, 2, 0); + layout.add(recordLocal, 1, row); + layout.add(recordRemote, 2, row++); recordLocation.selectedToggleProperty().addListener((e) -> { Config.getInstance().getSettings().localRecording = 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(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.textProperty().addListener((ob, o, n) -> { if(!server.getText().isEmpty()) { @@ -183,9 +186,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setFillWidth(server, true); GridPane.setHgrow(server, Priority.ALWAYS); 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.textProperty().addListener((observable, oldValue, newValue) -> { if (!newValue.matches("\\d*")) { @@ -199,14 +202,27 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setFillWidth(port, true); GridPane.setHgrow(port, Priority.ALWAYS); 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"); - layout.add(l, 0, 3); - secureCommunication.setSelected(Config.getInstance().getSettings().requireAuthentication); - secureCommunication.setOnAction((e) -> { - Config.getInstance().getSettings().requireAuthentication = secureCommunication.isSelected(); - if(secureCommunication.isSelected()) { + layout.add(l, 0, row); + useAuthentication.setSelected(Config.getInstance().getSettings().requireAuthentication); + useAuthentication.setOnAction((e) -> { + Config.getInstance().getSettings().requireAuthentication = useAuthentication.isSelected(); + if(useAuthentication.isSelected()) { byte[] key = Config.getInstance().getSettings().key; if(key == null) { 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(secureCommunication, new Insets(4, 0, 0, 0)); - layout.add(secureCommunication, 1, 3); + GridPane.setMargin(useAuthentication, new Insets(4, 0, 0, 0)); + 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); recordLocation.setCollapsible(false); @@ -582,7 +609,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { private void setRecordingMode(boolean local) { server.setDisable(local); port.setDisable(local); - secureCommunication.setDisable(local); + useAuthentication.setDisable(local); + useTLS.setDisable(local); recordingsDirectory.setDisable(!local); splitAfter.setDisable(!local); maxResolution.setDisable(!local); diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 7f015c83..f79ae2a5 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -11,6 +11,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -163,4 +164,23 @@ public class Config { 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; + } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index e0b119ea..2db52259 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -54,6 +54,7 @@ public class Settings { public String flirt4freeUsername; public boolean generatePlaylist = true; public int httpPort = 8080; + public int httpSecurePort = 8443; public String httpServer = "localhost"; public int httpTimeout = 10000; public String httpUserAgent = "Mozilla/5.0 Gecko/20100101 Firefox/62.0"; @@ -100,12 +101,14 @@ public class Settings { public String recordingsSortColumn = ""; public String recordingsSortType = ""; public boolean requireAuthentication = false; + public String servletContext = ""; public boolean showPlayerStarting = false; public boolean singlePlayer = true; public int splitRecordings = 0; public String startTab = "Settings"; public String streamatePassword = ""; public String streamateUsername = ""; + public boolean transportLayerSecurity = true; public int thumbWidth = 180; public boolean updateThumbnails = true; public String username = ""; // chaturbate username TODO maybe rename this onetime diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index e65e8983..646b0aff 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -6,6 +6,11 @@ import java.io.IOException; import java.net.Authenticator; import java.net.PasswordAuthentication; 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.List; import java.util.Map; @@ -13,6 +18,12 @@ import java.util.Map.Entry; import java.util.Set; 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.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(); } + /** + * 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() { persistCookies(); client.connectionPool().evictAll(); diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 3454a588..c9175013 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -38,6 +38,7 @@ import okhttp3.Response; public class RemoteRecorder implements Recorder { private static final transient Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class); + public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private Moshi moshi = new Moshi.Builder() .add(Instant.class, new InstantJsonAdapter()) @@ -71,6 +72,10 @@ public class RemoteRecorder implements Recorder { syncThread.start(); } + private String getRecordingEndpoint() { + return config.getServerUrl() + "/rec"; + } + @Override public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { sendRequest("start", model); @@ -86,7 +91,7 @@ public class RemoteRecorder implements Recorder { LOG.debug("Sending request to recording server: {}", payload); RequestBody body = RequestBody.create(JSON, payload); Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .url(getRecordingEndpoint()) .post(body); addHmacIfNeeded(payload, builder); Request request = builder.build(); @@ -172,7 +177,7 @@ public class RemoteRecorder implements Recorder { String msg = "{\"action\": \"space\"}"; RequestBody body = RequestBody.create(JSON, msg); Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .url(getRecordingEndpoint()) .post(body); addHmacIfNeeded(msg, builder); Request request = builder.build(); @@ -196,7 +201,7 @@ public class RemoteRecorder implements Recorder { String msg = "{\"action\": \"list\"}"; RequestBody body = RequestBody.create(JSON, msg); Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .url(getRecordingEndpoint()) .post(body); addHmacIfNeeded(msg, builder); Request request = builder.build(); @@ -231,7 +236,7 @@ public class RemoteRecorder implements Recorder { String msg = "{\"action\": \"listOnline\"}"; RequestBody body = RequestBody.create(JSON, msg); Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .url(getRecordingEndpoint()) .post(body); addHmacIfNeeded(msg, builder); Request request = builder.build(); @@ -265,7 +270,7 @@ public class RemoteRecorder implements Recorder { String msg = "{\"action\": \"recordings\"}"; RequestBody body = RequestBody.create(JSON, msg); Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .url(getRecordingEndpoint()) .post(body); addHmacIfNeeded(msg, builder); Request request = builder.build(); @@ -344,7 +349,7 @@ public class RemoteRecorder implements Recorder { String msg = recordingRequestAdapter.toJson(recReq); RequestBody body = RequestBody.create(JSON, msg); Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .url(getRecordingEndpoint()) .post(body); addHmacIfNeeded(msg, builder); Request request = builder.build(); @@ -472,7 +477,8 @@ public class RemoteRecorder implements Recorder { String msg = recordingRequestAdapter.toJson(recReq); LOG.debug(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); addHmacIfNeeded(msg, builder); Request request = builder.build(); diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index 2247df7a..72286c4f 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -89,6 +89,9 @@ public class MergedHlsDownload extends AbstractHlsDownload { URL u = new URL(segmentPlaylistUri); String path = u.getPath(); byte[] key = Config.getInstance().getSettings().key; + if(!Config.getInstance().getContextPath().isEmpty()) { + path = path.substring(Config.getInstance().getContextPath().length()); + } String hmac = Hmac.calculate(path, key); segmentPlaylistUri = segmentPlaylistUri + "?hmac=" + hmac; } diff --git a/common/src/main/java/ctbrec/servlet/StaticFileServlet.java b/common/src/main/java/ctbrec/servlet/StaticFileServlet.java index a0965651..0eeaf9dd 100644 --- a/common/src/main/java/ctbrec/servlet/StaticFileServlet.java +++ b/common/src/main/java/ctbrec/servlet/StaticFileServlet.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLConnection; +import java.util.HashMap; +import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -13,14 +15,19 @@ import javax.servlet.http.HttpServletResponse; public class StaticFileServlet extends HttpServlet { private String classPathRoot; + private Map mimetypes = new HashMap<>(); public StaticFileServlet(String classPathRoot) { this.classPathRoot = classPathRoot; + mimetypes.put("css", "text/css"); + mimetypes.put("js", "application/javascript"); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String request = req.getRequestURI(); + String contextPath = getServletContext().getContextPath(); + request = request.substring(contextPath.length()); serveFile(request, resp); } @@ -34,7 +41,12 @@ public class StaticFileServlet extends HttpServlet { if (resourceAsStream == null) { 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); OutputStream out = resp.getOutputStream(); int length = 0; @@ -43,4 +55,12 @@ public class StaticFileServlet extends HttpServlet { 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; + } } diff --git a/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java b/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java index ebca073e..2ec49a83 100644 --- a/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/AbstractCtbrecServlet.java @@ -20,6 +20,7 @@ public abstract class AbstractCtbrecServlet extends HttpServlet { String httpHeaderHmac = req.getHeader("CTBREC-HMAC"); String hmac = null; String url = req.getRequestURI(); + url = url.substring(getServletContext().getContextPath().length()); if(reqParamHmac != null) { hmac = reqParamHmac; diff --git a/server/src/main/java/ctbrec/recorder/server/HlsServlet.java b/server/src/main/java/ctbrec/recorder/server/HlsServlet.java index cf7b02db..51f89745 100644 --- a/server/src/main/java/ctbrec/recorder/server/HlsServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/HlsServlet.java @@ -32,7 +32,8 @@ public class HlsServlet extends AbstractCtbrecServlet { @Override 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 requestedFile = new File(recordingsDir, request); diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 1e34609b..de29f234 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -5,8 +5,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.BindException; +import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import javax.servlet.ServletException; 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.ServerConnector; 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.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +71,7 @@ public class HttpServer { logEnvironment(); createSites(); System.setProperty("ctbrec.server.mode", "1"); - if(System.getProperty("ctbrec.config") == null) { + if (System.getProperty("ctbrec.config") == null) { System.setProperty("ctbrec.config", "server.json"); } try { @@ -83,12 +86,12 @@ public class HttpServer { registerAlertSystem(); config = Config.getInstance(); - if(config.getSettings().key != null) { + if (config.getSettings().key != null) { LOG.info("HMAC authentication is enabled"); } recorder = new NextGenLocalRecorder(config, sites); for (Site site : sites) { - if(site.isEnabled()) { + if (site.isEnabled()) { site.init(); } } @@ -105,7 +108,7 @@ public class HttpServer { sites.add(new Fc2Live()); sites.add(new Flirt4Free()); sites.add(new LiveJasmin()); - //sites.add(new MyFreeCams()); + // sites.add(new MyFreeCams()); sites.add(new Streamate()); } @@ -114,10 +117,10 @@ public class HttpServer { @Override public void run() { LOG.info("Shutting down"); - if(onlineMonitor != null) { + if (onlineMonitor != null) { onlineMonitor.shutdown(); } - if(recorder != null) { + if (recorder != null) { recorder.shutdown(); } try { @@ -140,48 +143,62 @@ public class HttpServer { HttpConfiguration config = new HttpConfiguration(); 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.setIdleTimeout(this.config.getSettings().httpTimeout); - server.addConnector(http); - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.setContextPath("/secured/*"); - server.setHandler(context); + // connector for https (TLS) + ServerConnector https = new ServerConnector(server, sslContextFactory, httpConnectionFactory); + https.setPort(this.config.getSettings().httpSecurePort); + https.setIdleTimeout(this.config.getSettings().httpTimeout); - ServletHandler handler = new ServletHandler(); - //server.setHandler(handler); - HandlerList handlers = new HandlerList(); - handlers.setHandlers(new Handler[] { context, handler }); - server.setHandler(handlers); + String contextPath = Config.getInstance().getContextPath(); + ServletContextHandler basicAuthContext = new ServletContextHandler(ServletContextHandler.SESSIONS); + basicAuthContext.setContextPath(contextPath + "/secured"); + + ServletContextHandler defaultContext = new ServletContextHandler(ServletContextHandler.SESSIONS); + defaultContext.setContextPath(contextPath); RecorderServlet recorderServlet = new RecorderServlet(recorder, sites); ServletHolder holder = new ServletHolder(recorderServlet); - handler.addServletWithMapping(holder, "/rec"); + defaultContext.addServlet(holder, "/rec"); HlsServlet hlsServlet = new HlsServlet(this.config); holder = new ServletHolder(hlsServlet); - handler.addServletWithMapping(holder, "/hls/*"); - + defaultContext.addServlet(holder, "/hls/*"); if (this.config.getSettings().webinterface) { - LOG.info("Register static file servlet under {}", context.getContextPath()); StaticFileServlet staticFileServlet = new StaticFileServlet("/html"); holder = new ServletHolder(staticFileServlet); - handler.addServletWithMapping(holder, "/static/*"); - //context.addServlet(holder, "/"); + String staticFileContext = "/static/*"; + defaultContext.addServlet(holder, staticFileContext); + LOG.info("Register static file servlet under {}", defaultContext.getContextPath()+staticFileContext); // servlet to retrieve the HMAC secured by basic auth String username = this.config.getSettings().webinterfaceUsername; String password = this.config.getSettings().webinterfacePassword; - context.setSecurityHandler(basicAuth(username, password, "CTB Recorder")); - context.addServlet(new ServletHolder(new HttpServlet() { + basicAuthContext.setSecurityHandler(basicAuth(username, password, "CTB Recorder")); + basicAuthContext.addServlet(new ServletHolder(new HttpServlet() { @Override 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.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(); response.put("hmac", new String(hmac, "utf-8")); resp.getOutputStream().println(response.toString()); @@ -190,12 +207,25 @@ public class HttpServer { }), "/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 { server.start(); server.join(); } catch (BindException e) { LOG.error("Port {} is already in use", http.getPort(), e); 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")); try { LOG.debug("ctbrec server {}", getVersion().toString()); - } catch (IOException e) {} + } catch (IOException e) { + } } private Version getVersion() throws IOException { diff --git a/server/src/main/resources/html/static/index.html b/server/src/main/resources/html/static/index.html index f1979b0b..4962096a 100644 --- a/server/src/main/resources/html/static/index.html +++ b/server/src/main/resources/html/static/index.html @@ -11,27 +11,27 @@ CTB Recorder - + - + - - + + - + - + - + - +