rdp-pipe 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdp_pipe/__init__.py +1 -0
- rdp_pipe/activity_stamp.py +19 -0
- rdp_pipe/command_socket.py +414 -0
- rdp_pipe/config/__init__.py +1 -0
- rdp_pipe/config/entrypoint/__init__.py +1 -0
- rdp_pipe/config/entrypoint/rdpr.py +3 -0
- rdp_pipe/connection_error.py +126 -0
- rdp_pipe/connection_progress.py +53 -0
- rdp_pipe/connection_url.py +187 -0
- rdp_pipe/display_control.py +256 -0
- rdp_pipe/entrypoint/__init__.py +1 -0
- rdp_pipe/entrypoint/rdp.py +319 -0
- rdp_pipe/entrypoint/rdpr.py +303 -0
- rdp_pipe/fastpath_input.py +80 -0
- rdp_pipe/keyboard_debug.py +20 -0
- rdp_pipe/mouse_debug.py +20 -0
- rdp_pipe/pointer_debug.py +48 -0
- rdp_pipe/pointer_update.py +684 -0
- rdp_pipe/qt_session_mapping.py +28 -0
- rdp_pipe/qt_session_window.py +1722 -0
- rdp_pipe/rdp_connection.py +1305 -0
- rdp_pipe/rdp_input.py +111 -0
- rdp_pipe/rdp_session_core.py +468 -0
- rdp_pipe/rdp_session_thread.py +578 -0
- rdp_pipe/remote_session_end.py +57 -0
- rdp_pipe/runtime_paths.py +30 -0
- rdp_pipe/trust_store.py +446 -0
- rdp_pipe-0.1.0.dist-info/METADATA +264 -0
- rdp_pipe-0.1.0.dist-info/RECORD +33 -0
- rdp_pipe-0.1.0.dist-info/WHEEL +5 -0
- rdp_pipe-0.1.0.dist-info/entry_points.txt +3 -0
- rdp_pipe-0.1.0.dist-info/licenses/LICENSE +21 -0
- rdp_pipe-0.1.0.dist-info/top_level.txt +1 -0
rdp_pipe/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Desktop RDP client package built on aardwolf and PyQt6."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Touch a filepath when remote framebuffer updates arrive."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
_LOGGER = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def touch_activity_stamp_file(activity_stamp_filepath: str) -> None:
|
|
9
|
+
"""Create or update mtime on the activity stamp file."""
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
with open(activity_stamp_filepath, "a"):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
except OSError as error:
|
|
16
|
+
_LOGGER.warning(
|
|
17
|
+
"activity stamp touch failed filepath={activity_stamp_filepath} error={error}".format(
|
|
18
|
+
activity_stamp_filepath=activity_stamp_filepath,
|
|
19
|
+
error=error))
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Unix domain socket JSON-line command server for RDP automation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
|
|
14
|
+
import rdp_pipe.rdp_input
|
|
15
|
+
import rdp_pipe.rdp_session_core
|
|
16
|
+
import rdp_pipe.runtime_paths
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_LOGGER = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
COMMAND_HANDLER_TIMEOUT_SECONDS = 30.0
|
|
22
|
+
|
|
23
|
+
SUPPORTED_COMMANDS = (
|
|
24
|
+
"receive_screenshot",
|
|
25
|
+
"send_click",
|
|
26
|
+
"receive_geometry",
|
|
27
|
+
"send_geometry",
|
|
28
|
+
"send_key",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_success_response(result: dict) -> str:
|
|
33
|
+
"""Serialize a successful command response."""
|
|
34
|
+
|
|
35
|
+
response_body = {
|
|
36
|
+
"ok": True,
|
|
37
|
+
"result": result,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return json.dumps(response_body, separators=(",", ":"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_error_response(error_message: str) -> str:
|
|
44
|
+
"""Serialize a failed command response."""
|
|
45
|
+
|
|
46
|
+
response_body = {
|
|
47
|
+
"ok": False,
|
|
48
|
+
"error": error_message,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return json.dumps(response_body, separators=(",", ":"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_command_request(request_line: str) -> dict:
|
|
55
|
+
"""Parse one JSON-line command request."""
|
|
56
|
+
|
|
57
|
+
if request_line == "":
|
|
58
|
+
raise ValueError("empty request line")
|
|
59
|
+
|
|
60
|
+
request_body = json.loads(request_line)
|
|
61
|
+
|
|
62
|
+
if "command" not in request_body:
|
|
63
|
+
raise ValueError("request missing command field")
|
|
64
|
+
|
|
65
|
+
return request_body
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_command_name_from_request_line(request_line: str) -> str:
|
|
69
|
+
"""Return the command field from a request line, or unknown when not parseable."""
|
|
70
|
+
|
|
71
|
+
if request_line == "":
|
|
72
|
+
return "unknown"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
request_body = json.loads(request_line)
|
|
76
|
+
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
return "unknown"
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
return request_body["command"]
|
|
82
|
+
|
|
83
|
+
except KeyError:
|
|
84
|
+
return "unknown"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_command_transaction_log_line(
|
|
88
|
+
timestamp_text: str,
|
|
89
|
+
command_name: str,
|
|
90
|
+
request_size: int,
|
|
91
|
+
response_size: int,
|
|
92
|
+
response_success: bool,
|
|
93
|
+
transaction_duration_seconds: float) -> str:
|
|
94
|
+
"""Serialize one headless command transaction log entry as a JSON line."""
|
|
95
|
+
|
|
96
|
+
log_body = {
|
|
97
|
+
"timestamp": timestamp_text,
|
|
98
|
+
"command": command_name,
|
|
99
|
+
"request_size": request_size,
|
|
100
|
+
"response_size": response_size,
|
|
101
|
+
"response_success": response_success,
|
|
102
|
+
"transaction_duration_seconds": round(transaction_duration_seconds, 2),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
log_line = json.dumps(log_body, separators=(",", ":"))
|
|
106
|
+
log_line = log_line + "\n"
|
|
107
|
+
|
|
108
|
+
return log_line
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def write_command_transaction_log_line(log_line: str):
|
|
112
|
+
"""Write one transaction log JSON line to stdout for headless operators."""
|
|
113
|
+
|
|
114
|
+
sys.stdout.write(log_line)
|
|
115
|
+
sys.stdout.flush()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
DEFAULT_COMMAND_SOCKET_PATH = \
|
|
119
|
+
rdp_pipe.runtime_paths.build_default_command_socket_filepath()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CommandSocketClientError(Exception):
|
|
123
|
+
"""Remote command socket returned ok false or an invalid response envelope."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_command_request_body(command_name: str, **fields) -> dict:
|
|
127
|
+
"""Build one wire-protocol command request object."""
|
|
128
|
+
|
|
129
|
+
request_body = {"command": command_name}
|
|
130
|
+
|
|
131
|
+
for field_name in fields.keys():
|
|
132
|
+
request_body[field_name] = fields[field_name]
|
|
133
|
+
|
|
134
|
+
return request_body
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def send_command_request(socket_path: str, request_body: dict) -> dict:
|
|
138
|
+
"""Send one JSON-line command and return the parsed response envelope."""
|
|
139
|
+
|
|
140
|
+
request_line = json.dumps(request_body, separators=(",", ":"))
|
|
141
|
+
request_line = request_line + "\n"
|
|
142
|
+
|
|
143
|
+
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
144
|
+
client_socket.connect(socket_path)
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
client_socket.sendall(request_line.encode("utf-8"))
|
|
148
|
+
|
|
149
|
+
response_buffer = b""
|
|
150
|
+
|
|
151
|
+
while b"\n" not in response_buffer:
|
|
152
|
+
received_chunk = client_socket.recv(4096)
|
|
153
|
+
|
|
154
|
+
if received_chunk == b"":
|
|
155
|
+
raise ConnectionError(
|
|
156
|
+
"command socket closed before response line for command {command_name}".format(
|
|
157
|
+
command_name=request_body["command"]))
|
|
158
|
+
|
|
159
|
+
response_buffer = response_buffer + received_chunk
|
|
160
|
+
|
|
161
|
+
response_line = response_buffer.split(b"\n", 1)[0].decode("utf-8")
|
|
162
|
+
response_body = json.loads(response_line)
|
|
163
|
+
|
|
164
|
+
finally:
|
|
165
|
+
client_socket.close()
|
|
166
|
+
|
|
167
|
+
if "ok" not in response_body:
|
|
168
|
+
raise CommandSocketClientError(
|
|
169
|
+
"command socket response missing ok field for command {command_name}".format(
|
|
170
|
+
command_name=request_body["command"]))
|
|
171
|
+
|
|
172
|
+
if response_body["ok"] is False:
|
|
173
|
+
error_message = response_body["error"]
|
|
174
|
+
raise CommandSocketClientError(error_message)
|
|
175
|
+
|
|
176
|
+
return response_body
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class CommandSocketServer:
|
|
180
|
+
"""Serve newline-delimited JSON commands over a Unix domain socket."""
|
|
181
|
+
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
socket_path: str,
|
|
185
|
+
session: rdp_pipe.rdp_session_core.RdpAsyncSession,
|
|
186
|
+
*,
|
|
187
|
+
log_command_transactions: bool = False):
|
|
188
|
+
"""Bind session handlers to a Unix socket at socket_path."""
|
|
189
|
+
|
|
190
|
+
self._socket_path = socket_path
|
|
191
|
+
self._session = session
|
|
192
|
+
self._log_command_transactions = log_command_transactions
|
|
193
|
+
self._stop_event = threading.Event()
|
|
194
|
+
self._server_thread = None
|
|
195
|
+
self._listen_socket = None
|
|
196
|
+
|
|
197
|
+
def start(self):
|
|
198
|
+
"""Start accepting command socket clients on a background thread."""
|
|
199
|
+
|
|
200
|
+
self._server_thread = threading.Thread(
|
|
201
|
+
target=self._server_thread_main,
|
|
202
|
+
name="rdp-command-socket")
|
|
203
|
+
|
|
204
|
+
self._server_thread.start()
|
|
205
|
+
|
|
206
|
+
def set_session(self, session: rdp_pipe.rdp_session_core.RdpAsyncSession):
|
|
207
|
+
"""Point automation commands at a new session after reconnect."""
|
|
208
|
+
|
|
209
|
+
self._session = session
|
|
210
|
+
|
|
211
|
+
def stop(self):
|
|
212
|
+
"""Stop the accept loop and close the listening socket."""
|
|
213
|
+
|
|
214
|
+
self._stop_event.set()
|
|
215
|
+
|
|
216
|
+
if self._listen_socket is not None:
|
|
217
|
+
try:
|
|
218
|
+
self._listen_socket.close()
|
|
219
|
+
|
|
220
|
+
except OSError:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
if self._server_thread is not None:
|
|
224
|
+
self._server_thread.join(timeout=2.0)
|
|
225
|
+
|
|
226
|
+
def _server_thread_main(self):
|
|
227
|
+
"""Accept one client at a time and process JSON-line commands."""
|
|
228
|
+
|
|
229
|
+
if os.path.exists(self._socket_path):
|
|
230
|
+
os.unlink(self._socket_path)
|
|
231
|
+
|
|
232
|
+
self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
233
|
+
self._listen_socket.bind(self._socket_path)
|
|
234
|
+
self._listen_socket.listen(1)
|
|
235
|
+
|
|
236
|
+
while not self._stop_event.is_set():
|
|
237
|
+
try:
|
|
238
|
+
self._listen_socket.settimeout(1.0)
|
|
239
|
+
client_socket, _client_address = self._listen_socket.accept()
|
|
240
|
+
|
|
241
|
+
except (TimeoutError, socket.timeout):
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
except OSError:
|
|
245
|
+
if self._stop_event.is_set():
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
raise
|
|
249
|
+
|
|
250
|
+
self._serve_client(client_socket)
|
|
251
|
+
client_socket.close()
|
|
252
|
+
|
|
253
|
+
def _serve_client(self, client_socket: socket.socket):
|
|
254
|
+
"""Read requests from one client until disconnect."""
|
|
255
|
+
|
|
256
|
+
input_file = client_socket.makefile(mode="r", encoding="utf-8")
|
|
257
|
+
output_file = client_socket.makefile(mode="w", encoding="utf-8")
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
while not self._stop_event.is_set():
|
|
261
|
+
request_line = input_file.readline()
|
|
262
|
+
|
|
263
|
+
if request_line == "":
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
request_payload = request_line.rstrip("\n")
|
|
267
|
+
request_size = len(request_payload.encode("utf-8"))
|
|
268
|
+
transaction_started_at = time.perf_counter()
|
|
269
|
+
response_line = self._dispatch_request_line(request_payload)
|
|
270
|
+
transaction_duration_seconds = \
|
|
271
|
+
time.perf_counter() - transaction_started_at
|
|
272
|
+
|
|
273
|
+
output_file.write(response_line)
|
|
274
|
+
output_file.write("\n")
|
|
275
|
+
output_file.flush()
|
|
276
|
+
|
|
277
|
+
if self._log_command_transactions:
|
|
278
|
+
response_size = len(response_line.encode("utf-8"))
|
|
279
|
+
response_body = json.loads(response_line)
|
|
280
|
+
response_success = response_body["ok"]
|
|
281
|
+
command_name = extract_command_name_from_request_line(request_payload)
|
|
282
|
+
timestamp_text = \
|
|
283
|
+
datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat(
|
|
284
|
+
timespec="milliseconds")
|
|
285
|
+
log_line = build_command_transaction_log_line(
|
|
286
|
+
timestamp_text,
|
|
287
|
+
command_name,
|
|
288
|
+
request_size,
|
|
289
|
+
response_size,
|
|
290
|
+
response_success,
|
|
291
|
+
transaction_duration_seconds)
|
|
292
|
+
|
|
293
|
+
write_command_transaction_log_line(log_line)
|
|
294
|
+
|
|
295
|
+
finally:
|
|
296
|
+
input_file.close()
|
|
297
|
+
output_file.close()
|
|
298
|
+
|
|
299
|
+
def _dispatch_request_line(self, request_line: str) -> str:
|
|
300
|
+
"""Parse and execute one command request."""
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
request_body = parse_command_request(request_line)
|
|
304
|
+
|
|
305
|
+
except ValueError as error:
|
|
306
|
+
return build_error_response(str(error))
|
|
307
|
+
|
|
308
|
+
except json.JSONDecodeError as error:
|
|
309
|
+
return build_error_response(
|
|
310
|
+
"invalid JSON: {message}".format(message=str(error)))
|
|
311
|
+
|
|
312
|
+
command_name = request_body["command"]
|
|
313
|
+
|
|
314
|
+
if command_name not in SUPPORTED_COMMANDS:
|
|
315
|
+
return build_error_response(
|
|
316
|
+
"unknown command {command_name}".format(command_name=command_name))
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
result = self._execute_command_on_session_loop(request_body)
|
|
320
|
+
|
|
321
|
+
return build_success_response(result)
|
|
322
|
+
|
|
323
|
+
except rdp_pipe.rdp_input.RdpInputError as error:
|
|
324
|
+
return build_error_response(str(error))
|
|
325
|
+
|
|
326
|
+
except rdp_pipe.rdp_session_core.RdpSessionError as error:
|
|
327
|
+
return build_error_response(str(error))
|
|
328
|
+
|
|
329
|
+
except Exception as error:
|
|
330
|
+
traceback.print_exc()
|
|
331
|
+
|
|
332
|
+
return build_error_response(
|
|
333
|
+
"command failed: {message}".format(message=str(error)))
|
|
334
|
+
|
|
335
|
+
def _execute_command_on_session_loop(self, request_body: dict) -> dict:
|
|
336
|
+
"""Marshal command execution onto the RDP asyncio loop."""
|
|
337
|
+
|
|
338
|
+
event_loop = self._session.event_loop
|
|
339
|
+
if event_loop is None:
|
|
340
|
+
raise rdp_pipe.rdp_session_core.RdpSessionError(
|
|
341
|
+
"RDP session is reconnecting")
|
|
342
|
+
|
|
343
|
+
command_future = asyncio.run_coroutine_threadsafe(
|
|
344
|
+
self._handle_command(request_body),
|
|
345
|
+
event_loop)
|
|
346
|
+
|
|
347
|
+
return command_future.result(timeout=COMMAND_HANDLER_TIMEOUT_SECONDS)
|
|
348
|
+
|
|
349
|
+
async def _handle_command(self, request_body: dict) -> dict:
|
|
350
|
+
"""Dispatch a parsed command to the session."""
|
|
351
|
+
|
|
352
|
+
command_name = request_body["command"]
|
|
353
|
+
|
|
354
|
+
if command_name == "receive_screenshot":
|
|
355
|
+
image_format = "png"
|
|
356
|
+
if "format" in request_body:
|
|
357
|
+
image_format = request_body["format"]
|
|
358
|
+
|
|
359
|
+
quality = 9
|
|
360
|
+
if "quality" in request_body:
|
|
361
|
+
quality = int(request_body["quality"])
|
|
362
|
+
|
|
363
|
+
return await self._session.handle_receive_screenshot(
|
|
364
|
+
image_format,
|
|
365
|
+
quality)
|
|
366
|
+
|
|
367
|
+
if command_name == "receive_geometry":
|
|
368
|
+
return await self._session.handle_receive_geometry()
|
|
369
|
+
|
|
370
|
+
if command_name == "send_geometry":
|
|
371
|
+
width_value = None
|
|
372
|
+
height_value = None
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
width_value = int(request_body["width"])
|
|
376
|
+
except KeyError:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
height_value = int(request_body["height"])
|
|
381
|
+
except KeyError:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
return await self._session.handle_send_geometry(
|
|
385
|
+
width_value,
|
|
386
|
+
height_value)
|
|
387
|
+
|
|
388
|
+
if command_name == "send_click":
|
|
389
|
+
if "x" not in request_body or "y" not in request_body:
|
|
390
|
+
raise ValueError("send_click requires x and y")
|
|
391
|
+
|
|
392
|
+
button = "left"
|
|
393
|
+
if "button" in request_body:
|
|
394
|
+
button = request_body["button"]
|
|
395
|
+
|
|
396
|
+
return await self._session.handle_send_click(
|
|
397
|
+
int(request_body["x"]),
|
|
398
|
+
int(request_body["y"]),
|
|
399
|
+
button)
|
|
400
|
+
|
|
401
|
+
if command_name == "send_key":
|
|
402
|
+
keys_value = None
|
|
403
|
+
key_value = None
|
|
404
|
+
|
|
405
|
+
if "keys" in request_body:
|
|
406
|
+
keys_value = request_body["keys"]
|
|
407
|
+
|
|
408
|
+
if "key" in request_body:
|
|
409
|
+
key_value = request_body["key"]
|
|
410
|
+
|
|
411
|
+
return await self._session.handle_send_key(keys_value, key_value)
|
|
412
|
+
|
|
413
|
+
raise ValueError(
|
|
414
|
+
"unsupported command {command_name}".format(command_name=command_name))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration mirrors for rdp_pipe."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Entrypoint config mirrors for rdp_pipe operator CLIs."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Classify RDP connect failures and format operator-facing stderr messages."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import errno
|
|
5
|
+
import ssl
|
|
6
|
+
import urllib.parse
|
|
7
|
+
|
|
8
|
+
import rdp_pipe.rdp_session_core
|
|
9
|
+
import rdp_pipe.trust_store
|
|
10
|
+
|
|
11
|
+
DEFAULT_RDP_PORT = 3389
|
|
12
|
+
_SERVER_ERROR_PREFIX = "Server replied with error"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_connection_endpoint(connection_url: str) -> str:
|
|
16
|
+
"""Return host:port for operator messages parsed from a connection URL."""
|
|
17
|
+
|
|
18
|
+
parsed_url = urllib.parse.urlparse(connection_url)
|
|
19
|
+
hostname = parsed_url.hostname
|
|
20
|
+
|
|
21
|
+
if parsed_url.port is not None:
|
|
22
|
+
port = parsed_url.port
|
|
23
|
+
else:
|
|
24
|
+
port = DEFAULT_RDP_PORT
|
|
25
|
+
|
|
26
|
+
return "{hostname}:{port}".format(hostname=hostname, port=port)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def describe_connection_failure(error: BaseException) -> str:
|
|
30
|
+
"""Map a connect exception to a short English reason without a traceback."""
|
|
31
|
+
|
|
32
|
+
if isinstance(error, rdp_pipe.trust_store.CertificateTrustMismatchError):
|
|
33
|
+
return str(error)
|
|
34
|
+
|
|
35
|
+
if isinstance(error, asyncio.TimeoutError):
|
|
36
|
+
return "connection timed out"
|
|
37
|
+
|
|
38
|
+
if isinstance(error, ConnectionResetError):
|
|
39
|
+
return "connection lost"
|
|
40
|
+
|
|
41
|
+
if isinstance(error, BrokenPipeError):
|
|
42
|
+
return "connection lost"
|
|
43
|
+
|
|
44
|
+
if isinstance(error, OSError):
|
|
45
|
+
if error.errno == errno.ECONNREFUSED:
|
|
46
|
+
return "connection refused"
|
|
47
|
+
|
|
48
|
+
if error.errno == errno.EHOSTUNREACH:
|
|
49
|
+
return "no route to host"
|
|
50
|
+
|
|
51
|
+
if error.errno == errno.ENETUNREACH:
|
|
52
|
+
return "network unreachable"
|
|
53
|
+
|
|
54
|
+
if error.errno == errno.ETIMEDOUT:
|
|
55
|
+
return "connection timed out"
|
|
56
|
+
|
|
57
|
+
if error.strerror is not None and error.strerror != "":
|
|
58
|
+
return error.strerror
|
|
59
|
+
|
|
60
|
+
return str(error)
|
|
61
|
+
|
|
62
|
+
if isinstance(error, ssl.SSLError):
|
|
63
|
+
ssl_detail = str(error)
|
|
64
|
+
|
|
65
|
+
if ssl_detail != "":
|
|
66
|
+
return "TLS handshake failed: {detail}".format(detail=ssl_detail)
|
|
67
|
+
|
|
68
|
+
return "TLS handshake failed"
|
|
69
|
+
|
|
70
|
+
if isinstance(error, rdp_pipe.rdp_session_core.RdpSessionError):
|
|
71
|
+
return str(error)
|
|
72
|
+
|
|
73
|
+
error_message = str(error)
|
|
74
|
+
|
|
75
|
+
if _SERVER_ERROR_PREFIX in error_message:
|
|
76
|
+
return error_message
|
|
77
|
+
|
|
78
|
+
return "connection failed"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_connection_failure_stderr(
|
|
82
|
+
connection_url: str,
|
|
83
|
+
error: BaseException) -> str:
|
|
84
|
+
"""Return a managed stderr block for a failed connect attempt."""
|
|
85
|
+
|
|
86
|
+
if isinstance(error, rdp_pipe.trust_store.CertificateTrustMismatchError):
|
|
87
|
+
return rdp_pipe.trust_store.format_certificate_trust_mismatch_stderr(error)
|
|
88
|
+
|
|
89
|
+
endpoint = parse_connection_endpoint(connection_url)
|
|
90
|
+
failure_reason = describe_connection_failure(error)
|
|
91
|
+
|
|
92
|
+
return "error: could not connect to {endpoint} ({failure_reason})\n".format(
|
|
93
|
+
endpoint=endpoint,
|
|
94
|
+
failure_reason=failure_reason)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_session_ended_stderr(error: BaseException) -> str:
|
|
98
|
+
"""Return a managed stderr line when the session ends after connect."""
|
|
99
|
+
|
|
100
|
+
failure_reason = describe_connection_failure(error)
|
|
101
|
+
|
|
102
|
+
return "error: RDP session ended unexpectedly ({failure_reason})\n".format(
|
|
103
|
+
failure_reason=failure_reason)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def format_session_disconnected_reconnecting_stderr(connection_url: str) -> str:
|
|
107
|
+
"""Return a managed stderr line when a live session drops and reconnect begins."""
|
|
108
|
+
|
|
109
|
+
endpoint = parse_connection_endpoint(connection_url)
|
|
110
|
+
|
|
111
|
+
return "error: RDP session to {endpoint} disconnected; reconnecting...\n".format(
|
|
112
|
+
endpoint=endpoint)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def format_session_disconnected_with_reason_stderr(
|
|
116
|
+
connection_url: str,
|
|
117
|
+
errinfo_name: str) -> str:
|
|
118
|
+
"""Return stderr when the server sent SET_ERROR_INFO before the session dropped."""
|
|
119
|
+
|
|
120
|
+
endpoint = parse_connection_endpoint(connection_url)
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
"error: RDP session to {endpoint} ended ({errinfo_name}); "
|
|
124
|
+
"awaiting reconnect choice or reconnecting...\n".format(
|
|
125
|
+
endpoint=endpoint,
|
|
126
|
+
errinfo_name=errinfo_name))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Connection progress step identifiers and display labels for the GUI overlay."""
|
|
2
|
+
|
|
3
|
+
CONNECTION_STEP_RECONNECTING = "reconnecting"
|
|
4
|
+
CONNECTION_STEP_PREPARING = "preparing"
|
|
5
|
+
CONNECTION_STEP_CONNECTING = "connecting"
|
|
6
|
+
CONNECTION_STEP_VERIFYING_CERTIFICATE = "verifying_certificate"
|
|
7
|
+
CONNECTION_STEP_AUTHENTICATING = "authenticating"
|
|
8
|
+
CONNECTION_STEP_CONFIGURING_DISPLAY = "configuring_display"
|
|
9
|
+
CONNECTION_STEP_READY = "ready"
|
|
10
|
+
|
|
11
|
+
ORDERED_CONNECTION_STEPS = [
|
|
12
|
+
CONNECTION_STEP_RECONNECTING,
|
|
13
|
+
CONNECTION_STEP_PREPARING,
|
|
14
|
+
CONNECTION_STEP_CONNECTING,
|
|
15
|
+
CONNECTION_STEP_VERIFYING_CERTIFICATE,
|
|
16
|
+
CONNECTION_STEP_AUTHENTICATING,
|
|
17
|
+
CONNECTION_STEP_CONFIGURING_DISPLAY,
|
|
18
|
+
CONNECTION_STEP_READY,
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
CONNECTION_STEP_LABELS = {
|
|
22
|
+
CONNECTION_STEP_RECONNECTING: "Reconnecting to server",
|
|
23
|
+
CONNECTION_STEP_PREPARING: "Preparing connection",
|
|
24
|
+
CONNECTION_STEP_CONNECTING: "Connecting to server",
|
|
25
|
+
CONNECTION_STEP_VERIFYING_CERTIFICATE: "Verifying server certificate",
|
|
26
|
+
CONNECTION_STEP_AUTHENTICATING: "Authenticating",
|
|
27
|
+
CONNECTION_STEP_CONFIGURING_DISPLAY: "Configuring display",
|
|
28
|
+
CONNECTION_STEP_READY: "Ready",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_connection_step_label(step_identifier: str) -> str:
|
|
33
|
+
"""Return the operator-facing label for a connection progress step."""
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
return CONNECTION_STEP_LABELS[step_identifier]
|
|
37
|
+
|
|
38
|
+
except KeyError:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"unknown connection progress step {step_identifier}".format(
|
|
41
|
+
step_identifier=step_identifier))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_connection_step_index(step_identifier: str) -> int:
|
|
45
|
+
"""Return the zero-based index of a step in the connect sequence."""
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
return ORDERED_CONNECTION_STEPS.index(step_identifier)
|
|
49
|
+
|
|
50
|
+
except ValueError:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"unknown connection progress step {step_identifier}".format(
|
|
53
|
+
step_identifier=step_identifier))
|