Skip to content

Commit 9f3c0b7

Browse files
authored
Implement JSON RPC Signaling for Nethernet (#5)
* Add support for JSON-RPC signaling used by realms Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Fix javadoc Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com> * Bump version to 1.7.0 * Include transport-nethernet in GitHub release --------- Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
1 parent dcea2ba commit 9f3c0b7

File tree

6 files changed

+526
-317
lines changed

6 files changed

+526
-317
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
with:
3333
files: |
3434
transport-raknet/build/libs/*.jar
35+
transport-nethernet/build/libs/*.jar
3536
appID: ${{ secrets.RELEASE_APP_ID }}
3637
appPrivateKey: ${{ secrets.RELEASE_APP_PK }}
3738
discordWebhook: ${{ secrets.DISCORD_WEBHOOK }}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# Only update version on publishing to Maven Central
2-
version=1.6.1
2+
version=1.7.0

transport-nethernet/src/main/java/dev/kastle/netty/channel/nethernet/NetherNetConstants.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,25 @@ public class NetherNetConstants {
2727
public static final String RTC_NEGOTIATION_CANDIDATE_ADD = "CANDIDATEADD";
2828
public static final String RTC_NEGOTIATION_CONNECT_ERROR = "CONNECTERROR";
2929

30+
// Signaling User Agent String
31+
public static final String SIGNALING_USER_AGENT = "libHttpClient/1.0.0.0";
32+
3033
// Xbox Signaling Message Types
3134
public static final int XBOX_SIGNAL_NOT_FOUND = 0;
3235
public static final int XBOX_SIGNAL_SIGNAL = 1;
3336
public static final int XBOX_SIGNAL_CREDENTIALS = 2;
3437
public static final int XBOX_SIGNAL_ACCEPTED = 3;
3538
public static final int XBOX_SIGNAL_ACK = 4;
3639

40+
// Xbox JSON-RPC Signaling Method Names
41+
public static final String XBOX_RPC_METHOD_TURN_AUTH = "Signaling_TurnAuth_v1_0";
42+
public static final String XBOX_RPC_METHOD_SEND_MESSAGE = "Signaling_SendClientMessage_v1_0";
43+
public static final String XBOX_RPC_METHOD_RECEIVE_MESSAGE = "Signaling_ReceiveMessage_v1_0";
44+
public static final String XBOX_RPC_METHOD_PING = "System_Ping_v1_0";
45+
public static final String XBOX_RPC_METHOD_PONG = "System_Pong_v1_0";
46+
public static final String XBOX_RPC_INNER_METHOD_WEBRTC = "Signaling_WebRtc_v1_0";
47+
public static final String XBOX_RPC_INNER_METHOD_DELIVERY = "Signaling_DeliveryNotification_V1_0";
48+
3749
// SCTP Constants
3850
public static final int MAX_SCTP_MESSAGE_SIZE = 10000;
3951
public static final String RELIABLE_CHANNEL_LABEL = "ReliableDataChannel";
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package dev.kastle.netty.channel.nethernet.signaling;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import dev.kastle.netty.channel.nethernet.NetherNetConstants;
7+
import io.netty.bootstrap.Bootstrap;
8+
import io.netty.channel.socket.SocketChannel;
9+
import io.netty.channel.socket.nio.NioSocketChannel;
10+
import io.netty.channel.Channel;
11+
import io.netty.channel.ChannelHandlerContext;
12+
import io.netty.channel.ChannelInitializer;
13+
import io.netty.channel.ChannelPipeline;
14+
import io.netty.channel.EventLoopGroup;
15+
import io.netty.channel.SimpleChannelInboundHandler;
16+
import io.netty.channel.nio.NioEventLoopGroup;
17+
import io.netty.handler.codec.http.DefaultHttpHeaders;
18+
import io.netty.handler.codec.http.HttpClientCodec;
19+
import io.netty.handler.codec.http.HttpObjectAggregator;
20+
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
21+
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
22+
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
23+
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
24+
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
25+
import io.netty.handler.ssl.SslContext;
26+
import io.netty.handler.ssl.SslContextBuilder;
27+
import io.netty.util.internal.logging.InternalLogger;
28+
import io.netty.util.internal.logging.InternalLoggerFactory;
29+
30+
import java.net.ConnectException;
31+
import java.net.SocketAddress;
32+
import java.net.URI;
33+
import java.nio.channels.ClosedChannelException;
34+
import java.util.ArrayList;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.UUID;
38+
import java.util.concurrent.CompletableFuture;
39+
import java.util.concurrent.ConcurrentHashMap;
40+
41+
public abstract class AbstractNetherNetXboxSignaling extends SimpleChannelInboundHandler<TextWebSocketFrame>
42+
implements NetherNetClientSignaling, NetherNetServerSignaling {
43+
44+
protected final InternalLogger log = InternalLoggerFactory.getInstance(getClass());
45+
46+
protected final String xboxToken;
47+
protected final String localNetworkId;
48+
protected final URI uri;
49+
protected final EventLoopGroup eventLoopGroup;
50+
51+
protected Channel channel;
52+
protected CompletableFuture<List<IceServerInfo>> connectFuture;
53+
protected volatile List<IceServerInfo> iceServers = new ArrayList<>();
54+
55+
protected final Map<Long, SignalHandler> handlers = new ConcurrentHashMap<>();
56+
protected NetherNetServerSignaling.NewConnectionHandler newConnectionHandler;
57+
protected volatile NetherNetClientSignaling.NotFoundHandler notFoundHandler;
58+
59+
protected AbstractNetherNetXboxSignaling(String localNetworkId, String xboxToken, URI uri) {
60+
this.localNetworkId = localNetworkId;
61+
this.xboxToken = xboxToken;
62+
this.uri = uri;
63+
this.eventLoopGroup = new NioEventLoopGroup(1);
64+
}
65+
66+
@Override
67+
public String getLocalNetworkId() {
68+
return this.localNetworkId;
69+
}
70+
71+
@Override
72+
public synchronized CompletableFuture<List<IceServerInfo>> connect(SocketAddress remoteAddress) {
73+
return connectInternal();
74+
}
75+
76+
@Override
77+
public void bind(SocketAddress localAddress) throws ConnectException {
78+
try {
79+
connectInternal().join();
80+
} catch (Exception e) {
81+
Throwable cause = e.getCause() != null ? e.getCause() : e;
82+
close();
83+
if (cause instanceof ConnectException) throw (ConnectException) cause;
84+
ConnectException ce = new ConnectException("Failed to connect to Xbox Signaling: " + cause.getMessage());
85+
ce.initCause(cause);
86+
throw ce;
87+
}
88+
}
89+
90+
protected synchronized CompletableFuture<List<IceServerInfo>> connectInternal() {
91+
if (connectFuture != null) return connectFuture;
92+
93+
connectFuture = new CompletableFuture<>();
94+
connectFuture.thenAccept(servers -> this.iceServers = servers);
95+
96+
try {
97+
SslContext sslCtx = SslContextBuilder.forClient().build();
98+
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
99+
uri, WebSocketVersion.V13, null, false,
100+
new DefaultHttpHeaders()
101+
.add("Authorization", xboxToken)
102+
.add("User-Agent", NetherNetConstants.SIGNALING_USER_AGENT)
103+
.add("session-id", UUID.randomUUID().toString())
104+
.add("request-id", UUID.randomUUID().toString())
105+
);
106+
107+
Bootstrap b = new Bootstrap();
108+
b.group(eventLoopGroup)
109+
.channel(NioSocketChannel.class)
110+
.handler(new ChannelInitializer<SocketChannel>() {
111+
@Override
112+
protected void initChannel(SocketChannel ch) {
113+
ChannelPipeline p = ch.pipeline();
114+
p.addLast(sslCtx.newHandler(ch.alloc(), uri.getHost(), 443));
115+
p.addLast(new HttpClientCodec(), new HttpObjectAggregator(8192));
116+
p.addLast("ws-handshake", new WebSocketClientProtocolHandler(handshaker));
117+
p.addLast("handler", AbstractNetherNetXboxSignaling.this);
118+
}
119+
});
120+
121+
this.channel = b.connect(uri.getHost(), 443).sync().channel();
122+
} catch (Exception e) {
123+
Throwable cause = e.getCause() != null ? e.getCause() : e;
124+
if (connectFuture != null) connectFuture.completeExceptionally(cause);
125+
}
126+
return connectFuture;
127+
}
128+
129+
@Override
130+
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
131+
if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
132+
log.debug("{} WebSocket Connected", getClass().getSimpleName());
133+
onConnected(ctx);
134+
} else {
135+
super.userEventTriggered(ctx, evt);
136+
}
137+
}
138+
139+
/**
140+
* Called when the WebSocket handshake is complete.
141+
*/
142+
protected abstract void onConnected(ChannelHandlerContext ctx);
143+
144+
@Override
145+
public List<IceServerInfo> getIceServers() {
146+
return this.iceServers;
147+
}
148+
149+
@Override
150+
public void setNewConnectionHandler(NetherNetServerSignaling.NewConnectionHandler handler) {
151+
this.newConnectionHandler = handler;
152+
}
153+
154+
@Override
155+
public void setNotFoundHandler(NotFoundHandler handler) {
156+
this.notFoundHandler = handler;
157+
}
158+
159+
@Override
160+
public void setSignalHandler(long connectionId, SignalHandler handler) {
161+
this.handlers.put(connectionId, handler);
162+
}
163+
164+
@Override
165+
public void removeSignalHandler(long connectionId) {
166+
this.handlers.remove(connectionId);
167+
}
168+
169+
@Override
170+
public void setAdvertisementData(PongData pongData) {
171+
// No-op for Xbox Signaling.
172+
}
173+
174+
@Override
175+
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
176+
if (connectFuture != null && !connectFuture.isDone()) {
177+
connectFuture.completeExceptionally(cause);
178+
}
179+
log.error("Signaling Exception: {}", cause.getMessage(), cause);
180+
ctx.close();
181+
}
182+
183+
@Override
184+
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
185+
synchronized (this) {
186+
if (connectFuture != null && !connectFuture.isDone()) {
187+
connectFuture.completeExceptionally(new ClosedChannelException());
188+
}
189+
connectFuture = null;
190+
this.channel = null;
191+
}
192+
super.channelInactive(ctx);
193+
}
194+
195+
@Override
196+
public void close() {
197+
if (channel != null) channel.close();
198+
eventLoopGroup.shutdownGracefully();
199+
}
200+
201+
protected void dispatchSignalToPipeline(String sender, String rawMsg) {
202+
try {
203+
// Signal Format: <Type> <ConnectionID> <Data>
204+
String[] parts = rawMsg.split(" ", 3);
205+
if (parts.length < 2) return;
206+
207+
long connectionId = Long.parseUnsignedLong(parts[1]);
208+
209+
SignalHandler handler = handlers.get(connectionId);
210+
if (handler != null) {
211+
handler.onSignal(rawMsg);
212+
return;
213+
}
214+
215+
if (NetherNetConstants.RTC_NEGOTIATION_CONNECT_REQUEST.equals(parts[0]) && newConnectionHandler != null) {
216+
String payload = parts.length > 2 ? parts[2] : "";
217+
newConnectionHandler.onConnect(connectionId, sender, payload);
218+
} else {
219+
log.debug("No handler found for connection ID: {} (Type: {})", connectionId, parts[0]);
220+
}
221+
} catch (Exception e) {
222+
log.error("Failed to dispatch signal: {}", rawMsg, e);
223+
}
224+
}
225+
226+
protected List<IceServerInfo> parseTurnServers(JsonObject json) {
227+
List<IceServerInfo> result = new ArrayList<>();
228+
try {
229+
JsonArray servers = null;
230+
if (json.has("TurnAuthServers")) servers = json.getAsJsonArray("TurnAuthServers");
231+
else if (json.has("turnAuthServers")) servers = json.getAsJsonArray("turnAuthServers");
232+
233+
if (servers != null) {
234+
for (JsonElement el : servers) {
235+
JsonObject server = el.getAsJsonObject();
236+
List<String> urls = new ArrayList<>();
237+
238+
JsonArray urlsArray = null;
239+
if (server.has("Urls")) urlsArray = server.getAsJsonArray("Urls");
240+
else if (server.has("urls")) urlsArray = server.getAsJsonArray("urls");
241+
242+
if (urlsArray != null) {
243+
urlsArray.forEach(u -> urls.add(u.getAsString()));
244+
245+
IceServerInfo.Builder info = new IceServerInfo.Builder().setUrls(urls);
246+
247+
if (server.has("Username")) info.setUsername(server.get("Username").getAsString());
248+
else if (server.has("username")) info.setUsername(server.get("username").getAsString());
249+
250+
if (server.has("Password")) info.setPassword(server.get("Password").getAsString());
251+
else if (server.has("password")) info.setPassword(server.get("password").getAsString());
252+
else if (server.has("Credential")) info.setPassword(server.get("Credential").getAsString());
253+
else if (server.has("credential")) info.setPassword(server.get("credential").getAsString());
254+
255+
result.add(info.build());
256+
}
257+
}
258+
}
259+
} catch (Exception e) {
260+
log.error("Failed to parse TURN servers", e);
261+
}
262+
log.debug("Successfully parsed {} ICE servers.", result.size());
263+
return result;
264+
}
265+
}

0 commit comments

Comments
 (0)