-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRemoteControlSession.cs
More file actions
225 lines (194 loc) · 8.83 KB
/
Copy pathRemoteControlSession.cs
File metadata and controls
225 lines (194 loc) · 8.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
using System.Text.Json;
using SwitchifyPc.Core.Pairing;
using SwitchifyPc.Protocol;
namespace SwitchifyPc.Core.Control;
public sealed record RemoteSessionOutgoingMessage(string ConnectionId, string ResponseJson, bool CloseConnection = false);
public sealed record RemoteSessionResult(IReadOnlyList<RemoteSessionOutgoingMessage> OutgoingMessages)
{
public static RemoteSessionResult None { get; } = new([]);
public static RemoteSessionResult One(RemoteSessionOutgoingMessage message) => new([message]);
}
public sealed class RemoteControlSession
{
private readonly PairingManager pairingManager;
private readonly PairingApprovalManager pairingApprovalManager;
private readonly ControlSession commandSession;
private readonly Action? onPendingPairingRequestsChanged;
private readonly Dictionary<string, PendingConnection> pendingConnectionsByRequestId = new(StringComparer.Ordinal);
public RemoteControlSession(
PairingManager pairingManager,
PairingApprovalManager pairingApprovalManager,
ControlSession commandSession,
Action? onPendingPairingRequestsChanged = null)
{
this.pairingManager = pairingManager;
this.pairingApprovalManager = pairingApprovalManager;
this.commandSession = commandSession;
this.onPendingPairingRequestsChanged = onPendingPairingRequestsChanged;
}
public async Task<RemoteSessionResult> ProcessMessageAsync(
string connectionId,
string rawMessage,
string? remoteAddress = null,
CancellationToken cancellationToken = default)
{
using JsonDocument? document = TryParse(rawMessage, out string? invalidJsonResponse);
if (document is null)
{
return RemoteSessionResult.One(new RemoteSessionOutgoingMessage(connectionId, invalidJsonResponse!));
}
JsonElement request = document.RootElement;
ProtocolValidationResult validation = ProtocolValidator.ValidateProtocolRequest(request);
if (!validation.Ok)
{
return RemoteSessionResult.One(new RemoteSessionOutgoingMessage(
connectionId,
ErrorResponse(RequestIdOrNull(request), validation.Error ?? "invalid_message", validation.Message ?? "Message is invalid.")));
}
string type = request.GetProperty("type").GetString() ?? "";
if (type == "pairing.request")
{
return await HandlePairingRequestAsync(connectionId, request, remoteAddress, cancellationToken).ConfigureAwait(false);
}
ControlSessionResult commandResult = await commandSession.ProcessMessageAsync(rawMessage, cancellationToken).ConfigureAwait(false);
return commandResult.HasResponse
? RemoteSessionResult.One(new RemoteSessionOutgoingMessage(connectionId, commandResult.ResponseJson!))
: RemoteSessionResult.None;
}
public async Task<RemoteSessionResult> AcceptPairingRequestAsync(
string requestId,
CancellationToken cancellationToken = default)
{
PairingApprovalResponseResult result = await pairingApprovalManager.AcceptAsync(requestId, cancellationToken).ConfigureAwait(false);
PendingConnection? pendingConnection = ClearPendingConnection(requestId);
onPendingPairingRequestsChanged?.Invoke();
if (!result.Ok || pendingConnection is null)
{
return RemoteSessionResult.None;
}
return RemoteSessionResult.One(new RemoteSessionOutgoingMessage(
pendingConnection.ConnectionId,
ProtocolValidator.CreatePairingCompleteResponse(
requestId,
result.DesktopId ?? "",
result.DeviceId ?? "",
result.Token ?? "").ToJsonString()));
}
public RemoteSessionResult RejectPairingRequest(string requestId)
{
PairingApprovalResponseResult result = pairingApprovalManager.Reject(requestId);
PendingConnection? pendingConnection = ClearPendingConnection(requestId);
onPendingPairingRequestsChanged?.Invoke();
return result.Ok && pendingConnection is not null
? RemoteSessionResult.One(new RemoteSessionOutgoingMessage(
pendingConnection.ConnectionId,
ErrorResponse(requestId, "invalid_auth", "pairing_rejected"),
CloseConnection: true))
: RemoteSessionResult.None;
}
public RemoteSessionResult ExpirePendingPairingRequests()
{
List<RemoteSessionOutgoingMessage> outgoingMessages = [];
foreach (PendingPairingApproval request in pairingApprovalManager.ExpirePendingRequests())
{
PendingConnection? pendingConnection = ClearPendingConnection(request.RequestId);
if (pendingConnection is null) continue;
outgoingMessages.Add(new RemoteSessionOutgoingMessage(
pendingConnection.ConnectionId,
ErrorResponse(request.RequestId, "invalid_auth", "pairing_request_expired"),
CloseConnection: true));
}
if (outgoingMessages.Count > 0)
{
onPendingPairingRequestsChanged?.Invoke();
}
return new RemoteSessionResult(outgoingMessages);
}
public void RemoveConnection(string connectionId)
{
string[] requestIds = pendingConnectionsByRequestId
.Where(entry => entry.Value.ConnectionId == connectionId)
.Select(entry => entry.Key)
.ToArray();
foreach (string requestId in requestIds)
{
pendingConnectionsByRequestId.Remove(requestId);
pairingApprovalManager.Reject(requestId);
}
if (requestIds.Length > 0)
{
onPendingPairingRequestsChanged?.Invoke();
}
}
private async Task<RemoteSessionResult> HandlePairingRequestAsync(
string connectionId,
JsonElement request,
string? remoteAddress,
CancellationToken cancellationToken)
{
JsonElement payload = request.GetProperty("payload");
string desktopId = await pairingManager.GetDesktopIdAsync(cancellationToken).ConfigureAwait(false);
if (payload.GetProperty("desktopId").GetString() != desktopId)
{
return RemoteSessionResult.One(new RemoteSessionOutgoingMessage(
connectionId,
ErrorResponse(RequestIdOrNull(request), "invalid_auth", "pairing_mismatch")));
}
CreatePairingApprovalRequestResult result = pairingApprovalManager.CreateRequest(new CreatePairingApprovalRequestInput(
RequestId: request.GetProperty("id").GetString() ?? "",
DeviceId: payload.GetProperty("deviceId").GetString() ?? "",
DeviceName: payload.GetProperty("deviceName").GetString() ?? "",
DesktopId: desktopId,
RequestNonce: payload.GetProperty("requestNonce").GetString() ?? "",
RemoteAddress: remoteAddress));
List<RemoteSessionOutgoingMessage> outgoingMessages = [];
if (result.ReplacedRequestId is not null)
{
PendingConnection? replacedConnection = ClearPendingConnection(result.ReplacedRequestId);
if (replacedConnection is not null)
{
outgoingMessages.Add(new RemoteSessionOutgoingMessage(
replacedConnection.ConnectionId,
ErrorResponse(result.ReplacedRequestId, "invalid_auth", "pairing_request_expired"),
CloseConnection: true));
}
}
pendingConnectionsByRequestId[result.Request.RequestId] = new PendingConnection(connectionId);
onPendingPairingRequestsChanged?.Invoke();
return new RemoteSessionResult(outgoingMessages);
}
private PendingConnection? ClearPendingConnection(string requestId)
{
if (!pendingConnectionsByRequestId.Remove(requestId, out PendingConnection? pendingConnection))
{
return null;
}
return pendingConnection;
}
private static JsonDocument? TryParse(string rawMessage, out string? errorResponse)
{
try
{
errorResponse = null;
return JsonDocument.Parse(rawMessage);
}
catch (JsonException)
{
errorResponse = ErrorResponse(null, "invalid_json", "Message must be valid JSON.");
return null;
}
}
private static string ErrorResponse(string? id, string code, string message)
{
return ProtocolValidator.CreateErrorResponse(id, code, message).ToJsonString();
}
private static string? RequestIdOrNull(JsonElement request)
{
return request.ValueKind == JsonValueKind.Object &&
request.TryGetProperty("id", out JsonElement id) &&
id.ValueKind == JsonValueKind.String
? id.GetString()
: null;
}
private sealed record PendingConnection(string ConnectionId);
}