Implemented HMAC authentication in RemoteRecorder

This commit is contained in:
0xboobface 2018-07-07 13:23:00 +02:00
parent d62bba5599
commit bb6ba48f49
7 changed files with 93 additions and 46 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
/ctbrec.log /ctbrec.log
/ctbrec-tunnel.sh /ctbrec-tunnel.sh
/jre/ /jre/
/server-local.sh

View File

@ -82,7 +82,7 @@ public class LocalRecorder implements Recorder {
} }
@Override @Override
public void stopRecording(Model model) throws IOException, InterruptedException { public void stopRecording(Model model) throws IOException {
lock.lock(); lock.lock();
try { try {
if (models.contains(model)) { if (models.contains(model)) {
@ -130,7 +130,7 @@ public class LocalRecorder implements Recorder {
} }
} }
private void stopRecordingProcess(Model model) throws IOException, InterruptedException { private void stopRecordingProcess(Model model) throws IOException {
lock.lock(); lock.lock();
try { try {
Download download = recordingProcesses.get(model); Download download = recordingProcesses.get(model);

View File

@ -1,15 +1,17 @@
package ctbrec.recorder; package ctbrec.recorder;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
public interface Recorder { public interface Recorder {
public void startRecording(Model model) throws IOException; public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
public void stopRecording(Model model) throws IOException, InterruptedException; public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
/** /**
* Returns, if a model is in the list of models to record. This does not reflect, if there currently is a recording running. The model might be offline * Returns, if a model is in the list of models to record. This does not reflect, if there currently is a recording running. The model might be offline
@ -19,9 +21,9 @@ public interface Recorder {
public List<Model> getModelsRecording(); public List<Model> getModelsRecording();
public List<Recording> getRecordings() throws IOException; public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
public void delete(Recording recording) throws IOException; public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
public void shutdown(); public void shutdown();
} }

View File

@ -1,6 +1,9 @@
package ctbrec.recorder; package ctbrec.recorder;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -12,12 +15,14 @@ import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.HttpClient; import ctbrec.HttpClient;
import ctbrec.InstantJsonAdapter; import ctbrec.InstantJsonAdapter;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
@ -49,25 +54,26 @@ public class RemoteRecorder implements Recorder {
} }
@Override @Override
public void startRecording(Model model) throws IOException { public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
sendRequest("start", model); sendRequest("start", model);
} }
@Override @Override
public void stopRecording(Model model) throws IOException, InterruptedException { public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
sendRequest("stop", model); sendRequest("stop", model);
} }
private void sendRequest(String action, Model model) throws IOException { private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
String requestTemplate = "{\"action\": \"<<action>>\", \"model\": <<model>>}"; String requestTemplate = "{\"action\": \"<<action>>\", \"model\": <<model>>}";
requestTemplate = requestTemplate.replaceAll("<<action>>", action); requestTemplate = requestTemplate.replaceAll("<<action>>", action);
requestTemplate = requestTemplate.replaceAll("<<model>>", modelAdapter.toJson(model)); requestTemplate = requestTemplate.replaceAll("<<model>>", modelAdapter.toJson(model));
LOG.debug("Sending request to recording server: {}", requestTemplate); LOG.debug("Sending request to recording server: {}", requestTemplate);
RequestBody body = RequestBody.create(JSON, requestTemplate); RequestBody body = RequestBody.create(JSON, requestTemplate);
Request request = new Request.Builder() Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
.post(body) .post(body);
.build(); addHmacIfNeeded(requestTemplate, builder);
Request request = builder.build();
Response response = client.execute(request); Response response = client.execute(request);
String json = response.body().string(); String json = response.body().string();
if(response.isSuccessful()) { if(response.isSuccessful()) {
@ -86,6 +92,14 @@ public class RemoteRecorder implements Recorder {
} }
} }
private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {
if(Config.getInstance().getSettings().requireAuthentication) {
byte[] key = Config.getInstance().getSettings().key;
String hmac = Hmac.calculate(msg, key);
builder.addHeader("CTBREC-HMAC", hmac);
}
}
@Override @Override
public boolean isRecording(Model model) { public boolean isRecording(Model model) {
return models != null && models.contains(model); return models != null && models.contains(model);
@ -116,12 +130,14 @@ public class RemoteRecorder implements Recorder {
public void run() { public void run() {
running = true; running = true;
while(running) { while(running) {
RequestBody body = RequestBody.create(JSON, "{\"action\": \"list\"}");
Request request = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
.post(body)
.build();
try { try {
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")
.post(body);
addHmacIfNeeded(msg, builder);
Request request = builder.build();
Response response = client.execute(request); Response response = client.execute(request);
String json = response.body().string(); String json = response.body().string();
if(response.isSuccessful()) { if(response.isSuccessful()) {
@ -135,7 +151,7 @@ public class RemoteRecorder implements Recorder {
} else { } else {
LOG.error("Couldn't synchronize with server. HTTP status: {} - {}", response.code(), json); LOG.error("Couldn't synchronize with server. HTTP status: {} - {}", response.code(), json);
} }
} catch (IOException e) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
LOG.error("Couldn't synchronize with server", e); LOG.error("Couldn't synchronize with server", e);
} }
@ -170,13 +186,14 @@ public class RemoteRecorder implements Recorder {
} }
@Override @Override
public List<Recording> getRecordings() throws IOException { public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
RequestBody body = RequestBody.create(JSON, "{\"action\": \"recordings\"}"); String msg = "{\"action\": \"recordings\"}";
Request request = new Request.Builder() RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
.post(body) .post(body);
.build(); addHmacIfNeeded(msg, builder);
Request request = builder.build();
Response response = client.execute(request); Response response = client.execute(request);
String json = response.body().string(); String json = response.body().string();
if(response.isSuccessful()) { if(response.isSuccessful()) {
@ -196,22 +213,24 @@ public class RemoteRecorder implements Recorder {
} }
@Override @Override
public void delete(Recording recording) throws IOException { public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
RequestBody body = RequestBody.create(JSON, "{\"action\": \"delete\", \"recording\": \""+recording.getPath()+"\"}"); String msg = "{\"action\": \"delete\", \"recording\": \""+recording.getPath()+"\"}";
Request request = new Request.Builder() RequestBody body = RequestBody.create(JSON, msg);
Request.Builder builder = new Request.Builder()
.url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec")
.post(body) .post(body);
.build(); addHmacIfNeeded(msg, builder);
Request request = builder.build();
Response response = client.execute(request); Response response = client.execute(request);
String json = response.body().string(); String json = response.body().string();
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
if(response.isSuccessful()) { if(response.isSuccessful()) {
RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
if(!resp.status.equals("success")) { if(!resp.status.equals("success")) {
throw new IOException("Couldn't delete recording: " + resp.status + " " + resp.msg); throw new IOException("Couldn't delete recording: " + resp.msg);
} }
} else { } else {
throw new IOException("Couldn't delete recording: " + response.code() + " " + json); throw new IOException("Couldn't delete recording: " + resp.msg);
//throw new IOException("Couldn't delete recording: " + response.code() + " " + json);
} }
} }
} }

View File

@ -1,6 +1,8 @@
package ctbrec.ui; package ctbrec.ui;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -112,7 +114,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
m.setUrl("https://chaturbate.com/" + m.getName() + "/"); m.setUrl("https://chaturbate.com/" + m.getName() + "/");
try { try {
recorder.startRecording(m); recorder.startRecording(m);
} catch (IOException e1) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error"); alert.setTitle("Error");
alert.setHeaderText("Couldn't add model"); alert.setHeaderText("Couldn't add model");
@ -229,16 +231,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
try { try {
recorder.stopRecording(selected); recorder.stopRecording(selected);
observableModels.remove(selected); observableModels.remove(selected);
} catch (IOException e1) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't stop recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't stop recording");
alert.setContentText("I/O error while stopping the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} catch (InterruptedException e1) {
LOG.error("Couldn't stop recording", e1); LOG.error("Couldn't stop recording", e1);
Platform.runLater(() -> { Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);

View File

@ -10,6 +10,8 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -175,7 +177,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
protected Task<List<JavaFxRecording>> createTask() { protected Task<List<JavaFxRecording>> createTask() {
return new Task<List<JavaFxRecording>>() { return new Task<List<JavaFxRecording>>() {
@Override @Override
public List<JavaFxRecording> call() throws IOException { public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
List<JavaFxRecording> recordings = new ArrayList<>(); List<JavaFxRecording> recordings = new ArrayList<>();
for (Recording rec : recorder.getRecordings()) { for (Recording rec : recorder.getRecordings()) {
recordings.add(new JavaFxRecording(rec)); recordings.add(new JavaFxRecording(rec));
@ -446,7 +448,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
try { try {
recorder.delete(r); recorder.delete(r);
Platform.runLater(() -> observableRecordings.remove(r)); Platform.runLater(() -> observableRecordings.remove(r));
} catch (IOException e1) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Error while deleting recording", e1); LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1); showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
} finally { } finally {

View File

@ -2,11 +2,13 @@ package ctbrec.ui;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Hmac;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -20,6 +22,7 @@ import javafx.scene.control.PasswordField;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.layout.Border; import javafx.scene.layout.Border;
@ -43,6 +46,7 @@ public class SettingsTab extends Tab {
private TextField server; private TextField server;
private TextField port; private TextField port;
private CheckBox loadResolution; private CheckBox loadResolution;
private CheckBox secureCommunication;
private PasswordField password; private PasswordField password;
private RadioButton recordLocal; private RadioButton recordLocal;
private RadioButton recordRemote; private RadioButton recordRemote;
@ -169,6 +173,32 @@ public class SettingsTab extends Tab {
GridPane.setColumnSpan(port, 2); GridPane.setColumnSpan(port, 2);
layout.add(port, 1, row); layout.add(port, 1, row);
layout.add(new Label("Require authentication"), 0, ++row);
secureCommunication = new CheckBox();
secureCommunication.setSelected(Config.getInstance().getSettings().requireAuthentication);
secureCommunication.setOnAction((e) -> {
Config.getInstance().getSettings().requireAuthentication = secureCommunication.isSelected();
if(secureCommunication.isSelected()) {
byte[] key = Config.getInstance().getSettings().key;
if(key == null) {
key = Hmac.generateKey();
Config.getInstance().getSettings().key = key;
}
TextInputDialog keyDialog = new TextInputDialog();
keyDialog.setResizable(true);
keyDialog.setTitle("Server Authentication");
keyDialog.setHeaderText("A key has been generated");
keyDialog.setContentText("Add this setting to your server's config.json:\n");
keyDialog.getEditor().setText("\"key\": " + Arrays.toString(key));
keyDialog.getEditor().setEditable(false);
keyDialog.setWidth(800);
keyDialog.setHeight(200);
keyDialog.show();
}
});
layout.add(secureCommunication, 1, row);
server.setDisable(recordLocal.isSelected()); server.setDisable(recordLocal.isSelected());
port.setDisable(recordLocal.isSelected()); port.setDisable(recordLocal.isSelected());
} }