Skip to content

Commit 1c87273

Browse files
committed
Added password authentication with OBS websocket server.
1 parent d6f873b commit 1c87273

File tree

8 files changed

+327
-19
lines changed

8 files changed

+327
-19
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ First include the library in your project using Maven:
1818
To get started just instantiate the OBSRemoteController:
1919

2020
```java
21-
2221
OBSRemoteController controller = new OBSRemoteController("ws://localhost:4444", false);
2322

2423
if (controller.isFailed()) { // Awaits response from OBS
@@ -37,6 +36,21 @@ controller.registerConnectCallback(response -> {
3736
// Other requests...
3837
});
3938
```
39+
40+
#### Websocket server with authentication
41+
42+
If your OBS websocket server is secured with a password, pass the password as a string to the controller:
43+
```java
44+
OBSRemoteController controller = new OBSRemoteController("ws://localhost:4444", false, "myPassword");
45+
```
46+
47+
Catch any authentication errors by registering a callback for this:
48+
```java
49+
controller.registerConnectionFailedCallback(message -> {
50+
System.err.println("Failed to connect: " + message);
51+
})
52+
```
53+
4054
---
4155
## Supported requests and events
4256

src/main/java/net/twasi/obsremotejava/OBSCommunicator.java

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import net.twasi.obsremotejava.events.responses.SwitchScenesResponse;
1010
import net.twasi.obsremotejava.events.responses.TransitionBeginResponse;
1111
import net.twasi.obsremotejava.events.responses.TransitionEndResponse;
12+
import net.twasi.obsremotejava.requests.Authenticate.AuthenticateRequest;
13+
import net.twasi.obsremotejava.requests.Authenticate.AuthenticateResponse;
1214
import net.twasi.obsremotejava.requests.GetAuthRequired.GetAuthRequiredRequest;
1315
import net.twasi.obsremotejava.requests.GetAuthRequired.GetAuthRequiredResponse;
1416
import net.twasi.obsremotejava.requests.GetCurrentProfile.GetCurrentProfileRequest;
@@ -75,6 +77,10 @@
7577
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
7678
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
7779

80+
import java.nio.charset.StandardCharsets;
81+
import java.security.MessageDigest;
82+
import java.security.NoSuchAlgorithmException;
83+
import java.util.Base64;
7884
import java.util.HashMap;
7985
import java.util.Map;
8086
import java.util.concurrent.CountDownLatch;
@@ -84,6 +90,7 @@
8490
@WebSocket(maxTextMessageSize = 64 * 1024, maxIdleTime = 360000000)
8591
public class OBSCommunicator {
8692
private boolean debug;
93+
private final String password;
8794
private final CountDownLatch closeLatch;
8895
public final Map<String, Class> messageTypes = new HashMap<>();
8996

@@ -93,8 +100,9 @@ public class OBSCommunicator {
93100

94101
private Callback onConnect;
95102
private Callback onDisconnect;
103+
private StringCallback onConnectionFailed;
96104

97-
// Optinal callbacks
105+
// Optional callbacks
98106
private Callback onReplayStarted;
99107
private Callback onReplayStarting;
100108
private Callback onReplayStopped;
@@ -106,9 +114,14 @@ public class OBSCommunicator {
106114

107115
private GetVersionResponse versionInfo;
108116

109-
public OBSCommunicator(boolean debug) {
117+
public OBSCommunicator(boolean debug, String password) {
110118
this.closeLatch = new CountDownLatch(1);
111119
this.debug = debug;
120+
this.password = password;
121+
}
122+
123+
public OBSCommunicator(boolean debug) {
124+
this(debug, null);
112125
}
113126

114127
public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException {
@@ -166,12 +179,23 @@ public void onMessage(String msg) {
166179
GetAuthRequiredResponse authRequiredResponse = (GetAuthRequiredResponse) responseBase;
167180
if (authRequiredResponse.isAuthRequired()) {
168181
System.out.println("Authentication is required.");
169-
// TODO support authentication
182+
authenticateWithServer(authRequiredResponse.getChallenge(), authRequiredResponse.getSalt());
170183
} else {
171184
System.out.println("Authentication is not required. You're ready to go!");
172185
this.onConnect.run(versionInfo);
173186
}
174187
break;
188+
189+
case "AuthenticateResponse":
190+
AuthenticateResponse authenticateResponse = (AuthenticateResponse) responseBase;
191+
192+
if ("ok".equals(authenticateResponse.getStatus())) {
193+
this.onConnect.run(versionInfo);
194+
} else {
195+
this.onConnectionFailed.run("Failed to authenticate with password. Error: " + authenticateResponse.getError());
196+
}
197+
198+
break;
175199
default:
176200
if (callbacks.containsKey(type)) {
177201
callbacks.get(type).run(responseBase);
@@ -233,6 +257,37 @@ public void onMessage(String msg) {
233257
}
234258
}
235259

260+
private void authenticateWithServer(String challenge, String salt) {
261+
if (password == null) {
262+
System.err.println("Authentication required by server but no password set for client");
263+
this.onConnectionFailed.run("Authentication required by server but no password set for client");
264+
return;
265+
}
266+
267+
MessageDigest digest;
268+
try {
269+
digest = MessageDigest.getInstance("SHA-256");
270+
} catch (NoSuchAlgorithmException e) {
271+
System.err.println("Failed to perform password authentication with server");
272+
e.printStackTrace();
273+
this.onConnectionFailed.run("Failed to perform password authentication with server");
274+
return;
275+
}
276+
277+
// Generate authentication response secret
278+
String secretString = password + salt;
279+
byte[] secretHash = digest.digest(secretString.getBytes(StandardCharsets.UTF_8));
280+
String encodedSecret = Base64.getEncoder().encodeToString(secretHash);
281+
282+
String authResponseString = encodedSecret + challenge;
283+
byte[] authResponseHash = digest.digest(authResponseString.getBytes(StandardCharsets.UTF_8));
284+
String authResponse = Base64.getEncoder().encodeToString(authResponseHash);
285+
286+
// Send authentication response secret to server
287+
session.getRemote()
288+
.sendStringByFuture(new Gson().toJson(new AuthenticateRequest(this, authResponse)));
289+
}
290+
236291
public void registerOnConnect(Callback onConnect) {
237292
this.onConnect = onConnect;
238293
}
@@ -241,6 +296,10 @@ public void registerOnDisconnect(Callback onDisconnect) {
241296
this.onDisconnect = onDisconnect;
242297
}
243298

299+
public void registerOnConnectionFailed(StringCallback onConnectionFailed) {
300+
this.onConnectionFailed = onConnectionFailed;
301+
}
302+
244303
public void registerOnReplayStarted(Callback onReplayStarted) {
245304
this.onReplayStarted = onReplayStarted;
246305
}

src/main/java/net/twasi/obsremotejava/OBSRemoteController.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@ public class OBSRemoteController {
1818

1919
private boolean failed;
2020

21-
public OBSRemoteController(String address, boolean debug) {
21+
public OBSRemoteController(String address, boolean debug, String password) {
2222
this.address = address;
23-
communicator = new OBSCommunicator(debug);
23+
communicator = new OBSCommunicator(debug, password);
2424
client = new WebSocketClient();
2525
try {
2626
client.start();
2727

2828
URI uri = new URI(address);
2929
ClientUpgradeRequest request = new ClientUpgradeRequest();
3030
Future<Session> connection = client.connect(communicator, uri, request);
31-
System.out.printf("Connecting to: %s%n", uri);
31+
System.out.printf("Connecting to: %s%s%n", uri, (password != null ? " with password" : ""));
3232

3333
try {
3434
connection.get();
@@ -63,6 +63,10 @@ public void run() {
6363
}
6464
}
6565

66+
public OBSRemoteController(String address, boolean debug) {
67+
this(address, debug, null);
68+
}
69+
6670
public boolean isFailed() {
6771
return failed;
6872
}
@@ -79,6 +83,10 @@ public void registerDisconnectCallback(Callback onDisconnect) {
7983
communicator.registerOnDisconnect(onDisconnect);
8084
}
8185

86+
public void registerConnectionFailedCallback(StringCallback onConnectionFailed) {
87+
communicator.registerOnConnectionFailed(onConnectionFailed);
88+
}
89+
8290
public void registerReplayStartedCallback(Callback onReplayStarted) {
8391
communicator.registerOnReplayStarted(onReplayStarted);
8492
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package net.twasi.obsremotejava;
2+
3+
public interface StringCallback {
4+
void run(String message);
5+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package net.twasi.obsremotejava.requests.Authenticate;
2+
3+
import net.twasi.obsremotejava.OBSCommunicator;
4+
import net.twasi.obsremotejava.requests.BaseRequest;
5+
import net.twasi.obsremotejava.requests.RequestType;
6+
7+
public class AuthenticateRequest extends BaseRequest {
8+
private String auth;
9+
10+
public AuthenticateRequest(OBSCommunicator com, String auth) {
11+
super(RequestType.Authenticate);
12+
13+
this.auth = auth;
14+
15+
com.messageTypes.put(getMessageId(), AuthenticateResponse.class);
16+
}
17+
18+
public String getAuth() {
19+
return auth;
20+
}
21+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package net.twasi.obsremotejava.requests.Authenticate;
2+
3+
import net.twasi.obsremotejava.requests.ResponseBase;
4+
5+
public class AuthenticateResponse extends ResponseBase {}

src/main/java/net/twasi/obsremotejava/requests/RequestType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
public enum RequestType {
44
GetVersion,
55
GetAuthRequired,
6+
Authenticate,
67

78
SetCurrentScene,
89
GetSceneList,

0 commit comments

Comments
 (0)