mech-client 0.14.1__py3-none-any.whl → 0.15.1__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.
Files changed (49) hide show
  1. mech_client/__init__.py +1 -1
  2. mech_client/cli.py +258 -11
  3. mech_client/configs/mechs.json +110 -110
  4. mech_client/interact.py +6 -1
  5. mech_client/marketplace_interact.py +160 -42
  6. mech_client/safe.py +73 -0
  7. mech_client/subgraph.py +0 -11
  8. {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/METADATA +278 -224
  9. {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/RECORD +22 -48
  10. scripts/deposit_native.py +48 -16
  11. scripts/deposit_token.py +107 -31
  12. scripts/nvm_subscribe.py +14 -6
  13. scripts/nvm_subscription/contracts/base_contract.py +9 -1
  14. scripts/nvm_subscription/contracts/nft_sales.py +1 -3
  15. scripts/nvm_subscription/contracts/subscription_provider.py +2 -4
  16. scripts/nvm_subscription/contracts/token.py +23 -5
  17. scripts/nvm_subscription/manager.py +109 -16
  18. scripts/utils.py +2 -2
  19. scripts/whitelist.py +5 -1
  20. mech_client/helpers/acn/README.md +0 -76
  21. mech_client/helpers/acn/__init__.py +0 -30
  22. mech_client/helpers/acn/acn.proto +0 -71
  23. mech_client/helpers/acn/acn_pb2.py +0 -42
  24. mech_client/helpers/acn/custom_types.py +0 -224
  25. mech_client/helpers/acn/dialogues.py +0 -126
  26. mech_client/helpers/acn/message.py +0 -274
  27. mech_client/helpers/acn/protocol.yaml +0 -24
  28. mech_client/helpers/acn/serialization.py +0 -149
  29. mech_client/helpers/acn/tests/__init__.py +0 -20
  30. mech_client/helpers/acn/tests/test_acn.py +0 -256
  31. mech_client/helpers/acn/tests/test_acn_dialogues.py +0 -53
  32. mech_client/helpers/acn/tests/test_acn_messages.py +0 -117
  33. mech_client/helpers/acn_data_share/README.md +0 -32
  34. mech_client/helpers/acn_data_share/__init__.py +0 -32
  35. mech_client/helpers/acn_data_share/acn_data_share.proto +0 -17
  36. mech_client/helpers/acn_data_share/acn_data_share_pb2.py +0 -29
  37. mech_client/helpers/acn_data_share/dialogues.py +0 -115
  38. mech_client/helpers/acn_data_share/message.py +0 -213
  39. mech_client/helpers/acn_data_share/protocol.yaml +0 -21
  40. mech_client/helpers/acn_data_share/serialization.py +0 -111
  41. mech_client/helpers/acn_data_share/tests/test_acn_data_share_dialogues.py +0 -49
  42. mech_client/helpers/acn_data_share/tests/test_acn_data_share_messages.py +0 -53
  43. mech_client/helpers/p2p_libp2p_client/README.md +0 -15
  44. mech_client/helpers/p2p_libp2p_client/__init__.py +0 -21
  45. mech_client/helpers/p2p_libp2p_client/connection.py +0 -703
  46. mech_client/helpers/p2p_libp2p_client/connection.yaml +0 -52
  47. {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/LICENSE +0 -0
  48. {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/WHEEL +0 -0
  49. {mech_client-0.14.1.dist-info → mech_client-0.15.1.dist-info}/entry_points.txt +0 -0
@@ -1,703 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # ------------------------------------------------------------------------------
3
- #
4
- # Copyright 2022-2024 Valory AG
5
- # Copyright 2018-2019 Fetch.AI Limited
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- #
19
- # ------------------------------------------------------------------------------
20
- """This module contains the libp2p client connection."""
21
- # pylint: disable-all
22
- import asyncio
23
- import contextlib
24
- import hashlib
25
- import logging
26
- import random
27
- import ssl
28
- from asyncio import CancelledError
29
- from asyncio.events import AbstractEventLoop
30
- from asyncio.streams import StreamWriter
31
- from pathlib import Path
32
- from typing import Any, Dict, List, Optional
33
-
34
- from asn1crypto import x509 # type: ignore
35
- from ecdsa.curves import SECP256k1 # type: ignore
36
- from ecdsa.keys import BadSignatureError, VerifyingKey # type: ignore
37
- from ecdsa.util import sigdecode_der # type: ignore
38
-
39
- from aea.configurations.base import PublicId
40
- from aea.configurations.constants import DEFAULT_LEDGER
41
- from aea.connections.base import Connection, ConnectionStates
42
- from aea.crypto.registries import make_crypto
43
- from aea.exceptions import enforce
44
- from aea.helpers.acn.agent_record import AgentRecord
45
- from aea.helpers.acn.uri import Uri
46
- from aea.helpers.pipe import IPCChannelClient, TCPSocketChannelClient, TCPSocketProtocol
47
- from aea.mail.base import Envelope
48
-
49
- from packages.valory.protocols.acn import acn_pb2
50
- from packages.valory.protocols.acn.message import AcnMessage
51
-
52
-
53
- try:
54
- from asyncio.streams import ( # type: ignore # pylint: disable=ungrouped-imports
55
- IncompleteReadError,
56
- )
57
- except ImportError: # pragma: nocover
58
- from asyncio import IncompleteReadError # pylint: disable=ungrouped-imports
59
-
60
-
61
- _default_logger = logging.getLogger("aea.packages.valory.connections.p2p_libp2p_client")
62
-
63
- PUBLIC_ID = PublicId.from_str("valory/p2p_libp2p_client:0.1.0")
64
-
65
- SUPPORTED_LEDGER_IDS = ["fetchai", "cosmos", "ethereum"]
66
-
67
- POR_DEFAULT_SERVICE_ID = "acn"
68
-
69
- ACN_CURRENT_VERSION = "0.1.0"
70
-
71
-
72
- class NodeClient:
73
- """Client to communicate with node using ipc channel(pipe)."""
74
-
75
- ACN_ACK_TIMEOUT = 5.0
76
-
77
- def __init__(self, pipe: IPCChannelClient, node_por: AgentRecord) -> None:
78
- """Set node client with pipe."""
79
- self.pipe = pipe
80
- self._wait_status: Optional[asyncio.Future] = None
81
- self.agent_record = node_por
82
-
83
- async def wait_for_status(self) -> Any:
84
- """Get status."""
85
- if self._wait_status is None: # pragma: nocover
86
- raise ValueError("waiter for status not set!")
87
- return await asyncio.wait_for(self._wait_status, timeout=self.ACN_ACK_TIMEOUT)
88
-
89
- @staticmethod
90
- def make_acn_envelope_message(envelope: Envelope) -> bytes:
91
- """Make acn message with envelope in."""
92
- acn_msg = acn_pb2.AcnMessage() # type: ignore
93
- performative = acn_pb2.AcnMessage.Aea_Envelope_Performative() # type: ignore
94
- performative.envelope = envelope.encode()
95
- acn_msg.aea_envelope.CopyFrom(performative) # pylint: disable=no-member
96
- buf = acn_msg.SerializeToString()
97
- return buf
98
-
99
- async def write_acn_status_ok(self) -> None:
100
- """Send acn status ok."""
101
- acn_msg = acn_pb2.AcnMessage() # type: ignore
102
- performative = acn_pb2.AcnMessage.Status_Performative() # type: ignore
103
- status = AcnMessage.StatusBody(
104
- status_code=AcnMessage.StatusBody.StatusCode.SUCCESS, msgs=[]
105
- )
106
- AcnMessage.StatusBody.encode(
107
- performative.body, status # pylint: disable=no-member
108
- )
109
- acn_msg.status.CopyFrom(performative) # pylint: disable=no-member
110
- buf = acn_msg.SerializeToString()
111
- await self._write(buf)
112
-
113
- async def write_acn_status_error(
114
- self,
115
- msg: str,
116
- status_code: AcnMessage.StatusBody.StatusCode = AcnMessage.StatusBody.StatusCode.ERROR_GENERIC, # type: ignore
117
- ) -> None:
118
- """Send acn status error generic."""
119
- acn_msg = acn_pb2.AcnMessage() # type: ignore
120
- performative = acn_pb2.AcnMessage.Status_Performative() # type: ignore
121
- status = AcnMessage.StatusBody(status_code=status_code, msgs=[msg])
122
- AcnMessage.StatusBody.encode(
123
- performative.body, status # pylint: disable=no-member
124
- )
125
- acn_msg.status.CopyFrom(performative) # pylint: disable=no-member
126
-
127
- buf = acn_msg.SerializeToString()
128
-
129
- await self._write(buf)
130
-
131
- async def connect(self) -> bool:
132
- """Connect to node with pipe."""
133
- return await self.pipe.connect()
134
-
135
- async def send_envelope(self, envelope: Envelope) -> None:
136
- """Send envelope to node."""
137
- self._wait_status = asyncio.Future()
138
- buf = self.make_acn_envelope_message(envelope)
139
- await self._write(buf)
140
- try:
141
- status = await self.wait_for_status()
142
- if status.code != int(AcnMessage.StatusBody.StatusCode.SUCCESS): # type: ignore # pylint: disable=no-member
143
- raise ValueError( # pragma: nocover
144
- f"failed to send envelope. got error confirmation: {status}"
145
- )
146
- except asyncio.TimeoutError: # pragma: nocover
147
- if not self._wait_status.done(): # pragma: nocover
148
- self._wait_status.set_exception(Exception("Timeout"))
149
- await asyncio.sleep(0)
150
- raise ValueError("acn status await timeout!")
151
- finally:
152
- self._wait_status = None
153
-
154
- def make_agent_record(self) -> AcnMessage.AgentRecord: # type: ignore
155
- """Make acn agent record."""
156
- agent_record = AcnMessage.AgentRecord(
157
- address=self.agent_record.address,
158
- public_key=self.agent_record.public_key,
159
- peer_public_key=self.agent_record.representative_public_key,
160
- signature=self.agent_record.signature,
161
- service_id=POR_DEFAULT_SERVICE_ID,
162
- ledger_id=self.agent_record.ledger_id,
163
- )
164
- return agent_record
165
-
166
- async def read_envelope(self) -> Optional[Envelope]:
167
- """Read envelope from the node."""
168
- while True:
169
- buf = await self._read()
170
-
171
- if not buf:
172
- return None
173
-
174
- try:
175
- acn_msg = acn_pb2.AcnMessage() # type: ignore
176
- acn_msg.ParseFromString(buf)
177
-
178
- except Exception as e: # pragma: nocover
179
- await self.write_acn_status_error(
180
- f"Failed to parse acn message {e}",
181
- status_code=AcnMessage.StatusBody.StatusCode.ERROR_DECODE,
182
- )
183
- raise ValueError(f"Error parsing acn message: {e}") from e
184
-
185
- performative = acn_msg.WhichOneof("performative")
186
- if performative == "aea_envelope": # pragma: nocover
187
- aea_envelope = acn_msg.aea_envelope # pylint: disable=no-member
188
- try:
189
- envelope = Envelope.decode(aea_envelope.envelope)
190
- await self.write_acn_status_ok()
191
- return envelope
192
- except Exception as e:
193
- await self.write_acn_status_error(
194
- f"Failed to decode envelope: {e}",
195
- status_code=AcnMessage.StatusBody.StatusCode.ERROR_DECODE,
196
- )
197
- raise
198
-
199
- elif performative == "status":
200
- if self._wait_status is not None:
201
- self._wait_status.set_result(
202
- acn_msg.status.body # pylint: disable=no-member
203
- )
204
- else: # pragma: nocover
205
- await self.write_acn_status_error(
206
- f"Bad acn message {performative}",
207
- status_code=AcnMessage.StatusBody.StatusCode.ERROR_UNEXPECTED_PAYLOAD,
208
- )
209
-
210
- async def _write(self, data: bytes) -> None:
211
- """
212
- Write to the writer stream.
213
-
214
- :param data: data to write to stream
215
- """
216
- await self.pipe.write(data)
217
-
218
- async def _read(self) -> Optional[bytes]:
219
- """
220
- Read from the reader stream.
221
-
222
- :return: bytes
223
- """
224
- return await self.pipe.read()
225
-
226
- async def register(
227
- self,
228
- ) -> None:
229
- """Register agent on the remote node."""
230
- agent_record = self.make_agent_record()
231
- acn_msg = acn_pb2.AcnMessage() # type: ignore
232
- performative = acn_pb2.AcnMessage.Register_Performative() # type: ignore
233
- AcnMessage.AgentRecord.encode(
234
- performative.record, agent_record # pylint: disable=no-member
235
- )
236
- acn_msg.register.CopyFrom(performative) # pylint: disable=no-member
237
-
238
- buf = acn_msg.SerializeToString()
239
- await self._write(buf)
240
-
241
- try:
242
- buf = await asyncio.wait_for(self._read(), timeout=self.ACN_ACK_TIMEOUT)
243
- except ConnectionError as e: # pragma: nocover
244
- raise e
245
- except IncompleteReadError as e: # pragma: no cover
246
- raise e
247
-
248
- if buf is None: # pragma: nocover
249
- raise ConnectionError(
250
- "Error on connection setup. Incoming buffer is empty!"
251
- )
252
- acn_msg = acn_pb2.AcnMessage() # type: ignore
253
- acn_msg.ParseFromString(buf)
254
- performative = acn_msg.WhichOneof("performative")
255
- if performative != "status": # pragma: nocover
256
- raise Exception(f"Wrong response message from peer: {performative}")
257
- response = acn_msg.status # pylint: disable=no-member
258
-
259
- if response.body.code != int(AcnMessage.StatusBody.StatusCode.SUCCESS): # type: ignore # pylint: disable=no-member
260
- raise Exception( # pragma: nocover
261
- "Registration to peer failed: {}".format(
262
- AcnMessage.StatusBody.StatusCode(response.body.code) # type: ignore # pylint: disable=no-member
263
- )
264
- )
265
-
266
- async def close(self) -> None:
267
- """Close client and pipe."""
268
- await self.pipe.close()
269
-
270
-
271
- class P2PLibp2pClientConnection(Connection):
272
- """
273
- A libp2p client connection.
274
-
275
- Send and receive envelopes to and from agents on the p2p network without deploying a libp2p node.
276
- Connect to the libp2p node using traffic delegation service.
277
- """
278
-
279
- connection_id = PUBLIC_ID
280
-
281
- DEFAULT_CONNECT_RETRIES = 3
282
- DEFAULT_RESEND_ENVELOPE_RETRY = 1
283
- DEFAULT_TLS_CONNECTION_SIGNATURE_TIMEOUT = 5.0
284
-
285
- def __init__(self, **kwargs: Any) -> None:
286
- """Initialize a libp2p client connection."""
287
- super().__init__(**kwargs)
288
-
289
- self.tls_connection_signature_timeout = self.configuration.config.get(
290
- "tls_connection_signature_timeout",
291
- self.DEFAULT_TLS_CONNECTION_SIGNATURE_TIMEOUT,
292
- )
293
- self.connect_retries = self.configuration.config.get(
294
- "connect_retries", self.DEFAULT_CONNECT_RETRIES
295
- )
296
- self.resend_envelope_retry = self.configuration.config.get(
297
- "resend_envelope_retry", self.DEFAULT_RESEND_ENVELOPE_RETRY
298
- )
299
- ledger_id = self.configuration.config.get("ledger_id", DEFAULT_LEDGER)
300
- if ledger_id not in SUPPORTED_LEDGER_IDS:
301
- raise ValueError( # pragma: nocover
302
- "Ledger id '{}' is not supported. Supported ids: '{}'".format(
303
- ledger_id, SUPPORTED_LEDGER_IDS
304
- )
305
- )
306
-
307
- key_file: Optional[str] = self.configuration.config.get("tcp_key_file")
308
- nodes: Optional[List[Dict[str, Any]]] = self.configuration.config.get("nodes")
309
-
310
- if nodes is None:
311
- raise ValueError("At least one node should be provided")
312
- nodes = list(nodes)
313
-
314
- nodes_uris = [node.get("uri", None) for node in nodes]
315
- enforce(
316
- len(nodes_uris) == len(nodes) and None not in nodes_uris,
317
- "Delegate 'uri' should be provided for each node",
318
- )
319
-
320
- nodes_public_keys = [node.get("public_key", None) for node in nodes]
321
- enforce(
322
- len(nodes_public_keys) == len(nodes) and None not in nodes_public_keys,
323
- "Delegate 'public_key' should be provided for each node",
324
- )
325
-
326
- cert_requests = self.configuration.cert_requests
327
- if cert_requests is None or len(cert_requests) != len(nodes):
328
- raise ValueError( # pragma: nocover
329
- "cert_requests field must be set and contain exactly as many entries as 'nodes'!"
330
- )
331
- for cert_request in cert_requests:
332
- save_path = cert_request.get_absolute_save_path(Path(self.data_dir))
333
- if not save_path.is_file():
334
- raise Exception( # pragma: nocover
335
- f"cert_request 'save_path' field is not a file:\n{save_path}\n"
336
- "Please ensure that 'issue-certificates' command is called beforehand"
337
- )
338
-
339
- # we cannot use the key from the connection's crypto store as
340
- # the key will be used for TLS tcp connection, whereas the
341
- # connection's crypto store key is used for PoR
342
- if key_file is not None:
343
- key = make_crypto(ledger_id, private_key_path=key_file)
344
- else:
345
- key = make_crypto(ledger_id)
346
-
347
- # client connection id
348
- self.key = key
349
- self.logger.debug("Public key used for TCP: {}".format(key.public_key))
350
-
351
- # delegate uris
352
- self.delegate_uris = [Uri(node_uri) for node_uri in nodes_uris]
353
-
354
- # delegates PoRs
355
- self.delegate_pors: List[AgentRecord] = []
356
- for i, cert_request in enumerate(cert_requests):
357
- agent_record = AgentRecord.from_cert_request(
358
- cert_request, self.address, nodes_public_keys[i], self.data_dir
359
- )
360
- self.delegate_pors.append(agent_record)
361
-
362
- # select a delegate
363
- index = random.randint(0, len(self.delegate_uris) - 1) # nosec
364
- self.node_uri = self.delegate_uris[index]
365
- self.node_por = self.delegate_pors[index]
366
- self.logger.debug("Node to use as delegate: {}".format(self.node_uri))
367
-
368
- self._in_queue = None # type: Optional[asyncio.Queue]
369
- self._process_messages_task = None # type: Optional[asyncio.Future]
370
- self._node_client: Optional[NodeClient] = None
371
-
372
- self._send_queue: Optional[asyncio.Queue] = None
373
- self._send_task: Optional[asyncio.Task] = None
374
-
375
- async def _send_loop(self) -> None:
376
- """Handle message in the send queue."""
377
-
378
- if not self._send_queue or not self._node_client: # pragma: nocover
379
- self.logger.error("Send loop not started cause not connected properly.")
380
- return
381
- try:
382
- while self.is_connected:
383
- envelope = await self._send_queue.get()
384
- await self._send_envelope_with_node_client(envelope)
385
- except asyncio.CancelledError: # pylint: disable=try-except-raise
386
- raise # pragma: nocover
387
- except Exception: # pylint: disable=broad-except # pragma: nocover
388
- self.logger.exception(
389
- f"Failed to send an envelope {envelope}. Stop connection."
390
- )
391
- await asyncio.shield(self.disconnect())
392
-
393
- async def _send_envelope_with_node_client(
394
- self, envelope: Envelope, retry_counter: int = 0
395
- ) -> None:
396
- """Send envelope with node client, reconnect and retry on fail."""
397
- if not self._node_client: # pragma: nocover
398
- raise ValueError("Connection not connected to node!")
399
- if retry_counter > self.resend_envelope_retry:
400
- self.logger.warning(
401
- f"Dropping envelope {envelope}. It failed after retry. "
402
- )
403
- return
404
- self._ensure_valid_envelope_for_external_comms(envelope)
405
- try:
406
- await self._node_client.send_envelope(envelope)
407
- except Exception: # pylint: disable=broad-except
408
- self.logger.exception(
409
- "Exception raised on message send. Try reconnect and send again."
410
- )
411
- await self._perform_connection_to_node()
412
- await self._send_envelope_with_node_client(envelope, retry_counter + 1)
413
-
414
- async def connect(self) -> None:
415
- """Set up the connection."""
416
- if self.is_connected: # pragma: nocover
417
- return
418
-
419
- with self._connect_context():
420
- # connect libp2p client
421
-
422
- await self._perform_connection_to_node()
423
- # start receiving msgs
424
- self._in_queue = asyncio.Queue()
425
- self._process_messages_task = asyncio.ensure_future(
426
- self._process_messages(), loop=self.loop
427
- )
428
- self._send_queue = asyncio.Queue()
429
- self._send_task = self.loop.create_task(self._send_loop())
430
-
431
- async def _perform_connection_to_node(self) -> None:
432
- """Connect to node with retries."""
433
- for attempt in range(self.connect_retries):
434
- if self.state not in [ # type: ignore
435
- ConnectionStates.connecting,
436
- ConnectionStates.connected,
437
- ]:
438
- # do nothing if disconnected, or disconnecting
439
- return # pragma: nocover
440
- try:
441
- self.logger.info(
442
- "Connecting to libp2p node {}. Attempt {}".format(
443
- str(self.node_uri), attempt + 1
444
- )
445
- )
446
- pipe = TCPSocketChannelClientTLS(
447
- f"{self.node_uri.host}:{self.node_uri._port}", # pylint: disable=protected-access
448
- "",
449
- server_pub_key=self.node_por.representative_public_key,
450
- verification_signature_wait_timeout=self.tls_connection_signature_timeout,
451
- )
452
- if not await pipe.connect():
453
- raise ValueError(
454
- f"Pipe connection error: {pipe.last_exception or ''}"
455
- )
456
-
457
- self._node_client = NodeClient(pipe, self.node_por)
458
- await self._setup_connection()
459
-
460
- self.logger.info(
461
- "Successfully connected to libp2p node {}".format(
462
- str(self.node_uri)
463
- )
464
- )
465
- return
466
- except Exception as e: # pylint: disable=broad-except
467
- if attempt == self.connect_retries - 1:
468
- self.logger.error(
469
- "Connection to libp2p node {} failed: error: {}. It was the last attempt, exception will be raised".format(
470
- str(self.node_uri), str(e)
471
- )
472
- )
473
- self.state = ConnectionStates.disconnected
474
- raise
475
- sleep_time = attempt * 2 + 1
476
- self.logger.error(
477
- "Connection to libp2p node {} failed: error: {}. Another attempt will be performed in {} seconds".format(
478
- str(self.node_uri), str(e), sleep_time
479
- )
480
- )
481
- await asyncio.sleep(sleep_time)
482
-
483
- async def _setup_connection(self) -> None:
484
- """Set up connection to node over tcp connection."""
485
- if not self._node_client: # pragma: nocover
486
- raise ValueError("Connection was not connected!")
487
-
488
- await self._node_client.register()
489
-
490
- async def disconnect(self) -> None:
491
- """Disconnect from the channel."""
492
- if self.is_disconnected: # pragma: nocover
493
- return
494
-
495
- self.state = ConnectionStates.disconnecting
496
- self.logger.debug("disconnecting libp2p client connection...")
497
-
498
- if self._process_messages_task is not None:
499
- if not self._process_messages_task.done():
500
- self._process_messages_task.cancel()
501
- self._process_messages_task = None
502
-
503
- if self._send_task is not None:
504
- if not self._send_task.done():
505
- self._send_task.cancel()
506
- self._send_task = None
507
-
508
- try:
509
- self.logger.debug("disconnecting libp2p node client connection...")
510
- if self._node_client is not None:
511
- await self._node_client.close()
512
- except Exception: # pragma: nocover # pylint:disable=broad-except
513
- self.logger.exception("exception on node client close")
514
- raise
515
- finally:
516
- # set disconnected state anyway
517
- if self._in_queue is not None:
518
- self._in_queue.put_nowait(None)
519
-
520
- self.state = ConnectionStates.disconnected
521
- self.logger.debug("libp2p client connection disconnected.")
522
-
523
- async def receive(self, *args: Any, **kwargs: Any) -> Optional["Envelope"]:
524
- """
525
- Receive an envelope. Blocking.
526
-
527
- :param args: positional arguments
528
- :param kwargs: keyword arguments
529
- :return: the envelope received, or None.
530
- """
531
- try:
532
- if self._in_queue is None:
533
- raise ValueError("Input queue not initialized.") # pragma: nocover
534
- envelope = await self._in_queue.get()
535
- if envelope is None: # pragma: no cover
536
- self.logger.debug("Received None.")
537
- return None
538
- self.logger.debug("Received envelope: {}".format(envelope))
539
- return envelope
540
- except CancelledError: # pragma: no cover
541
- self.logger.debug("Receive cancelled.")
542
- return None
543
- except Exception as e: # pragma: no cover # pylint: disable=broad-except
544
- self.logger.exception(e)
545
- return None
546
-
547
- async def send(self, envelope: Envelope) -> None:
548
- """
549
- Send messages.
550
-
551
- :param envelope: the envelope
552
- """
553
- if not self._node_client or not self._send_queue:
554
- raise ValueError("Node is not connected!") # pragma: nocover
555
-
556
- self._ensure_valid_envelope_for_external_comms(envelope)
557
- await self._send_queue.put(envelope)
558
-
559
- async def _read_envelope_from_node(self) -> Optional[Envelope]:
560
- """Read envelope from node, reconnec on error."""
561
- if not self._node_client: # pragma: nocover
562
- raise ValueError("Connection not connected to node!")
563
-
564
- try:
565
- self.logger.debug("Waiting for messages...")
566
- envelope = await self._node_client.read_envelope()
567
- return envelope
568
- except ConnectionError as e: # pragma: nocover
569
- self.logger.error(f"Connection error: {e}. Try to reconnect and read again")
570
- except IncompleteReadError as e: # pragma: no cover
571
- self.logger.error(
572
- "Connection disconnected while reading from node ({}/{})".format(
573
- len(e.partial), e.expected
574
- )
575
- )
576
- except Exception as e: # pylint: disable=broad-except # pragma: nocover
577
- self.logger.exception(f"On envelope read: {e}")
578
-
579
- try:
580
- self.logger.debug("Read envelope retry! Reconnect first!")
581
- await self._perform_connection_to_node()
582
- envelope = await self._node_client.read_envelope()
583
- return envelope
584
- except Exception: # pragma: no cover # pylint: disable=broad-except
585
- self.logger.exception("Failed to read with reconnect!")
586
- return None
587
-
588
- async def _process_messages(self) -> None:
589
- """Receive data from node."""
590
- if not self._node_client: # pragma: nocover
591
- raise ValueError("Connection not connected to node!")
592
-
593
- while True:
594
- envelope = await self._read_envelope_from_node()
595
- if self._in_queue is None:
596
- raise ValueError("Input queue not initialized.") # pragma: nocover
597
- self.logger.debug(f"Received envelope: {envelope}")
598
- if envelope is None:
599
- # give it time to recover
600
- # twice the amount what we wait for ACK timeouts
601
- timeout = NodeClient.ACN_ACK_TIMEOUT * 2
602
- await asyncio.sleep(timeout)
603
- continue # pragma: no cover
604
- self._in_queue.put_nowait(envelope)
605
-
606
-
607
- class TCPSocketChannelClientTLS(TCPSocketChannelClient):
608
- """Interprocess communication channel client using tcp sockets with TLS."""
609
-
610
- DEFAULT_VERIFICATION_SIGNATURE_WAIT_TIMEOUT = 5.0
611
-
612
- def __init__(
613
- self,
614
- in_path: str,
615
- out_path: str,
616
- server_pub_key: str,
617
- logger: logging.Logger = _default_logger,
618
- loop: Optional[AbstractEventLoop] = None,
619
- verification_signature_wait_timeout: Optional[float] = None,
620
- ) -> None:
621
- """
622
- Initialize a tcp socket communication channel client.
623
-
624
- :param in_path: rendezvous point for incoming data
625
- :param out_path: rendezvous point for outgoing data
626
- :param server_pub_key: str, server public key to verify identity
627
- :param logger: the logger
628
- :param loop: the event loop
629
- :param verification_signature_wait_timeout: optional float, if not provided, default value will be used
630
- """
631
- super().__init__(in_path, out_path, logger, loop)
632
- self.verification_signature_wait_timeout = (
633
- self.DEFAULT_VERIFICATION_SIGNATURE_WAIT_TIMEOUT
634
- if verification_signature_wait_timeout is None
635
- else verification_signature_wait_timeout
636
- )
637
- self.server_pub_key = server_pub_key
638
-
639
- @staticmethod
640
- def _get_session_pub_key(writer: StreamWriter) -> bytes: # pragma: nocover
641
- """Get session public key from tls stream writer."""
642
- cert_data = writer.get_extra_info("ssl_object").getpeercert(binary_form=True)
643
-
644
- cert = x509.Certificate.load(cert_data)
645
- session_pub_key = VerifyingKey.from_der(cert.public_key.dump()).to_string(
646
- "uncompressed"
647
- )
648
- return session_pub_key
649
-
650
- async def _open_connection(self) -> TCPSocketProtocol:
651
- """Open a connection with TLS support and verify peer."""
652
- sock = await self._open_tls_connection()
653
- session_pub_key = self._get_session_pub_key(sock.writer)
654
-
655
- try:
656
- signature = await asyncio.wait_for(
657
- sock.read(), timeout=self.verification_signature_wait_timeout
658
- )
659
- except asyncio.TimeoutError: # pragma: nocover
660
- raise ValueError(
661
- f"Failed to get peer verification record in timeout: {self.verification_signature_wait_timeout}"
662
- )
663
-
664
- if not signature: # pragma: nocover
665
- raise ValueError("Unexpected socket read data!")
666
-
667
- try:
668
- self._verify_session_key_signature(signature, session_pub_key)
669
- except BadSignatureError as e: # pragma: nocover
670
- with contextlib.suppress(Exception):
671
- await sock.close()
672
- raise ValueError(f"Invalid TLS session key signature: {e}")
673
- return sock
674
-
675
- async def _open_tls_connection(self) -> TCPSocketProtocol:
676
- """Open a connection with TLS support."""
677
- cadata = await asyncio.get_event_loop().run_in_executor(
678
- None, lambda: ssl.get_server_certificate((self._host, self._port))
679
- )
680
-
681
- ssl_ctx = ssl.create_default_context(cadata=cadata)
682
- ssl_ctx.check_hostname = False
683
- ssl_ctx.verify_mode = ssl.CERT_REQUIRED
684
- reader, writer = await asyncio.open_connection(
685
- self._host,
686
- self._port,
687
- ssl=ssl_ctx,
688
- )
689
- return TCPSocketProtocol(reader, writer, logger=self.logger, loop=self._loop)
690
-
691
- def _verify_session_key_signature(
692
- self, signature: bytes, session_pub_key: bytes
693
- ) -> None:
694
- """
695
- Validate signature of session public key.
696
-
697
- :param signature: bytes, signature of session public key made with server private key
698
- :param session_pub_key: session public key to check signature for.
699
- """
700
- vk = VerifyingKey.from_string(bytes.fromhex(self.server_pub_key), SECP256k1)
701
- vk.verify(
702
- signature, session_pub_key, hashfunc=hashlib.sha256, sigdecode=sigdecode_der
703
- )