From bb02b5fd9f1eb3111c0473dc33f43ce92053294b Mon Sep 17 00:00:00 2001
From: 0xboobface <0xboobface@gmail.com>
Date: Sun, 4 Aug 2019 12:51:13 +0200
Subject: [PATCH] Add HMAC support to the webinterface
The webinterface didn't work, if HMAC authentication was enabled.
To make this work, the webinterface downloads the HMAC from the
server and stores it in the local storage of the browser. The
download URL is secured by Basic Auth. The credentials are configured
in the server.json
---
CHANGELOG.md | 7 +-
common/src/main/java/ctbrec/Hmac.java | 10 ++-
common/src/main/java/ctbrec/Settings.java | 2 +
.../ctbrec/servlet/StaticFileServlet.java | 4 +-
.../ctbrec/recorder/server/HttpServer.java | 75 ++++++++++++++++--
.../src/main/resources/html/static/index.html | 77 +++++++++++++++----
.../html/static/vendor/CryptoJS/base64.min.js | 1 +
.../static/vendor/CryptoJS/hmac-sha256.js | 18 +++++
8 files changed, 170 insertions(+), 24 deletions(-)
create mode 100644 server/src/main/resources/html/static/vendor/CryptoJS/base64.min.js
create mode 100644 server/src/main/resources/html/static/vendor/CryptoJS/hmac-sha256.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69c141c7..0b6fe1ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,14 @@
+2.2.0
+========================
+* Added HMAC authentication support to the webinterface
+
+
2.1.0
========================
This release is mainly for the server.
* Added webinterface for the server. Has to be activated in the server config
- (webinterface). You can access it via http://host:4711/static/index.html
+ (webinterface). You can access it via http://host:port/static/index.html
* Disabled MyFreeCams for the time being. (If you are brave, you can still
use an older version, but don't blame me, if your account or IP is getting
blocked)
diff --git a/common/src/main/java/ctbrec/Hmac.java b/common/src/main/java/ctbrec/Hmac.java
index 46f1751f..f58370b6 100644
--- a/common/src/main/java/ctbrec/Hmac.java
+++ b/common/src/main/java/ctbrec/Hmac.java
@@ -4,6 +4,7 @@ import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -16,11 +17,11 @@ public class Hmac {
private static final transient Logger LOG = LoggerFactory.getLogger(Hmac.class);
public static byte[] generateKey() {
- LOG.debug("Generating key");
+ LOG.debug("Generating HMAC key");
SecureRandom random = new SecureRandom();
byte[] key = new byte[32];
random.nextBytes(key);
- return key;
+ return Base64.getEncoder().encode(key);
}
public static String calculate(String msg, byte[] key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException, UnsupportedEncodingException {
@@ -42,7 +43,10 @@ public class Hmac {
* @param hash
* @return string
*/
- static String bytesToHex(byte[] hash) {
+ public static String bytesToHex(byte[] hash) {
+ if (hash == null) {
+ return "";
+ }
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xff & hash[i]);
diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java
index d9f8fd1a..e0b119ea 100644
--- a/common/src/main/java/ctbrec/Settings.java
+++ b/common/src/main/java/ctbrec/Settings.java
@@ -115,4 +115,6 @@ public class Settings {
public int windowX;
public int windowY;
public boolean webinterface = false;
+ public String webinterfaceUsername = "ctbrec";
+ public String webinterfacePassword = "sucks";
}
diff --git a/common/src/main/java/ctbrec/servlet/StaticFileServlet.java b/common/src/main/java/ctbrec/servlet/StaticFileServlet.java
index 5d47fc54..a0965651 100644
--- a/common/src/main/java/ctbrec/servlet/StaticFileServlet.java
+++ b/common/src/main/java/ctbrec/servlet/StaticFileServlet.java
@@ -31,7 +31,7 @@ public class StaticFileServlet extends HttpServlet {
private void serveFile(String resource, HttpServletResponse resp) throws IOException {
InputStream resourceAsStream = getClass().getResourceAsStream(classPathRoot + resource);
- if(resourceAsStream == null) {
+ if (resourceAsStream == null) {
throw new FileNotFoundException();
}
resp.setContentType(URLConnection.guessContentTypeFromName(resource));
@@ -39,7 +39,7 @@ public class StaticFileServlet extends HttpServlet {
OutputStream out = resp.getOutputStream();
int length = 0;
byte[] buffer = new byte[1024];
- while( (length = resourceAsStream.read(buffer)) >= 0 ) {
+ while ((length = resourceAsStream.read(buffer)) >= 0) {
out.write(buffer, 0, length);
}
}
diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java
index 638f7275..1e34609b 100644
--- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java
+++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java
@@ -8,17 +8,34 @@ import java.net.BindException;
import java.util.ArrayList;
import java.util.List;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.security.ConstraintMapping;
+import org.eclipse.jetty.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.security.HashLoginService;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.security.UserStore;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
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.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.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Objects;
+
import ctbrec.Config;
import ctbrec.Version;
import ctbrec.event.EventBusHolder;
@@ -128,10 +145,14 @@ public class HttpServer {
http.setIdleTimeout(this.config.getSettings().httpTimeout);
server.addConnector(http);
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ context.setContextPath("/secured/*");
+ server.setHandler(context);
+
ServletHandler handler = new ServletHandler();
- server.setHandler(handler);
+ //server.setHandler(handler);
HandlerList handlers = new HandlerList();
- handlers.setHandlers(new Handler[] { handler });
+ handlers.setHandlers(new Handler[] { context, handler });
server.setHandler(handlers);
RecorderServlet recorderServlet = new RecorderServlet(recorder, sites);
@@ -142,12 +163,31 @@ public class HttpServer {
holder = new ServletHolder(hlsServlet);
handler.addServletWithMapping(holder, "/hls/*");
+
if (this.config.getSettings().webinterface) {
- String staticContext = "/static/*";
- LOG.info("Register static file servlet under {}", staticContext);
+ LOG.info("Register static file servlet under {}", context.getContextPath());
StaticFileServlet staticFileServlet = new StaticFileServlet("/html");
holder = new ServletHolder(staticFileServlet);
- handler.addServletWithMapping(holder, staticContext);
+ handler.addServletWithMapping(holder, "/static/*");
+ //context.addServlet(holder, "/");
+
+ // 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() {
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ if(Objects.equal(username, req.getRemoteUser())) {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType("application/json");
+ byte[] hmac = HttpServer.this.config.getSettings().key;
+ JSONObject response = new JSONObject();
+ response.put("hmac", new String(hmac, "utf-8"));
+ resp.getOutputStream().println(response.toString());
+ }
+ }
+ }), "/hmac");
}
try {
@@ -159,6 +199,31 @@ public class HttpServer {
}
}
+ private static final SecurityHandler basicAuth(String username, String password, String realm) {
+ UserStore userStore = new UserStore();
+ userStore.addUser(username, Credential.getCredential(password), new String[] { "user" });
+ HashLoginService l = new HashLoginService();
+ l.setUserStore(userStore);
+ l.setName(realm);
+
+ Constraint constraint = new Constraint();
+ constraint.setName(Constraint.__BASIC_AUTH);
+ constraint.setRoles(new String[] { "user" });
+ constraint.setAuthenticate(true);
+
+ ConstraintMapping cm = new ConstraintMapping();
+ cm.setConstraint(constraint);
+ cm.setPathSpec("/*");
+
+ ConstraintSecurityHandler csh = new ConstraintSecurityHandler();
+ csh.setAuthenticator(new BasicAuthenticator());
+ csh.setRealmName("myrealm");
+ csh.addConstraintMapping(cm);
+ csh.setLoginService(l);
+
+ return csh;
+ }
+
private void registerAlertSystem() {
for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) {
EventHandler handler = new EventHandler(config);
diff --git a/server/src/main/resources/html/static/index.html b/server/src/main/resources/html/static/index.html
index 1d0f17f3..f1979b0b 100644
--- a/server/src/main/resources/html/static/index.html
+++ b/server/src/main/resources/html/static/index.html
@@ -207,11 +207,12 @@
+
-
+
+
+