atomicshop 2.17.3__py3-none-any.whl → 2.18.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.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

@@ -10,6 +10,6 @@ class ParserParent:
10
10
 
11
11
  def parse(self):
12
12
  # This is general parser, so we don't parse anything and 'request_body_parsed' gets empty byte string.
13
- self.class_client_message.request_body_parsed = b''
13
+ self.class_client_message.request_custom_parsed = b''
14
14
 
15
- self.logger.info(f"Parsed: {self.class_client_message.request_body_parsed[0: 100]}...")
15
+ self.logger.info(f"Parsed: {self.class_client_message.request_custom_parsed[0: 100]}...")
@@ -1,35 +1,45 @@
1
1
  import os
2
2
  from datetime import datetime
3
3
  import json
4
+ import queue
5
+ import threading
4
6
 
5
7
  from ...shared_functions import build_module_names, create_custom_logger
6
8
  from ... import message, recs_files
7
9
  from .... import filesystem
8
- from ....file_io import file_io
10
+ from ....file_io import jsons
11
+ from ....print_api import print_api
9
12
 
10
13
 
11
14
  # The class that is responsible for Recording Requests / Responses.
12
15
  class RecorderParent:
13
16
 
14
- def __init__(self, class_client_message: message.ClientMessage, record_path: str):
15
- self.class_client_message: message.ClientMessage = class_client_message
17
+ # noinspection PyTypeChecker
18
+ def __init__(self, record_path: str):
16
19
  self.record_path: str = record_path
20
+
17
21
  self.file_extension: str = ".json"
18
22
  self.engine_name = None
19
23
  self.module_name = None
20
24
  self.engine_record_path: str = str()
21
25
  self.record_file_path: str = str()
26
+ self.class_client_message: message.ClientMessage = None
22
27
 
23
28
  self.logger = create_custom_logger()
24
29
 
25
30
  # Get engine name and module name
26
31
  self.get_engine_module()
27
- # Build full file path.
28
- self.build_record_full_file_path()
32
+
33
+ # Build the record path with file name
34
+ self.build_record_path_to_engine()
29
35
 
30
36
  # Create folder.
31
37
  filesystem.create_directory(self.engine_record_path)
32
38
 
39
+ # Initialize a queue to hold messages
40
+ self.message_queue: queue.Queue = queue.Queue()
41
+ self.recorder_worker_thread = None
42
+
33
43
  # "self.__module__" is fully qualified module name: classes.engines.ENGINE-NAME.MODULE-NAME
34
44
  def get_engine_module(self):
35
45
  _, self.engine_name, self.module_name = build_module_names(self.__module__)
@@ -43,12 +53,9 @@ class RecorderParent:
43
53
  # Formatting the date and time and converting it to string object
44
54
  day_time_format: str = now.strftime(recs_files.REC_FILE_DATE_TIME_FORMAT)
45
55
 
46
- # Build the record path with file name
47
- self.build_record_path_to_engine()
48
-
49
56
  # If HTTP Path is not defined, 'http_path' will be empty, and it will not interfere with file name.
50
57
  self.record_file_path: str = (
51
- f"{self.engine_record_path}{os.sep}{day_time_format}_"
58
+ f"{self.engine_record_path}{os.sep}{day_time_format}_th{self.class_client_message.thread_id}_"
52
59
  f"{self.class_client_message.server_name}{self.file_extension}")
53
60
 
54
61
  def convert_messages(self):
@@ -59,27 +66,79 @@ class RecorderParent:
59
66
  # We need to check that the values that we want to convert aren't empty or 'None'.
60
67
  if self.class_client_message.request_raw_bytes:
61
68
  self.class_client_message.request_raw_hex = self.class_client_message.request_raw_bytes.hex()
62
- if self.class_client_message.response_list_of_raw_bytes:
63
- # We checked that the list isn't empty, now we check if the first value is not empty, since if we
64
- # check it in the same expression as check for list is empty, we will get an exception since
65
- # the list is empty.
66
- if self.class_client_message.response_list_of_raw_bytes[0]:
67
- for response_raw_bytes in self.class_client_message.response_list_of_raw_bytes:
68
- self.class_client_message.response_list_of_raw_hex.append(response_raw_bytes.hex())
69
-
70
- def record(self):
69
+ if self.class_client_message.response_raw_bytes:
70
+ self.class_client_message.response_raw_hex = self.class_client_message.response_raw_bytes.hex()
71
+
72
+ def record(self, class_client_message: message.ClientMessage):
73
+ self.class_client_message = class_client_message
74
+
75
+ # Build full file path if it is not already built.
76
+ if not self.record_file_path:
77
+ self.build_record_full_file_path()
78
+
79
+ # Start the worker thread if it is not already running
80
+ if not self.recorder_worker_thread:
81
+ self.recorder_worker_thread = threading.Thread(
82
+ target=save_message_worker,
83
+ args=(self.record_file_path, self.message_queue, self.logger),
84
+ name=f"Thread-{self.class_client_message.thread_id}_Recorder",
85
+ daemon=True
86
+ )
87
+ self.recorder_worker_thread.start()
88
+
71
89
  self.logger.info("Recording Message...")
72
90
 
73
91
  # Convert the requests and responses to hex.
74
92
  self.convert_messages()
75
93
  # Get the message in dict / JSON format
76
94
  record_message_dict: dict = dict(self.class_client_message)
77
- recorded_message_json_string = json.dumps(record_message_dict)
78
95
 
79
- # Since we already dumped the object to dictionary string, we'll just save the object to regular file.
80
- file_io.write_file(
81
- recorded_message_json_string, self.record_file_path, enable_long_file_path=True, **{'logger': self.logger})
82
-
83
- self.logger.info(f"Recorded to file: {self.record_file_path}")
96
+ # Put the message in the queue to be processed by the worker thread
97
+ self.message_queue.put(record_message_dict)
84
98
 
85
99
  return self.record_file_path
100
+
101
+
102
+ def save_message_worker(
103
+ record_file_path: str,
104
+ message_queue: queue.Queue,
105
+ logger
106
+ ):
107
+ """Worker function to process messages from the queue and write them to the file."""
108
+ while True:
109
+ # Get a message from the queue
110
+ record_message_dict = message_queue.get()
111
+
112
+ # Check for the "stop" signal
113
+ if record_message_dict is None:
114
+ break
115
+
116
+ # Read existing data from the file
117
+ try:
118
+ with open(record_file_path, 'r') as f:
119
+ current_json_file = json.load(f)
120
+ except FileNotFoundError:
121
+ current_json_file: list = []
122
+
123
+ # Append the new message to the existing data
124
+ final_json_list_of_dicts: list[dict] = []
125
+ if isinstance(current_json_file, list):
126
+ current_json_file.append(record_message_dict)
127
+ final_json_list_of_dicts = current_json_file
128
+ elif isinstance(current_json_file, dict):
129
+ final_json_list_of_dicts.append(current_json_file)
130
+ final_json_list_of_dicts.append(record_message_dict)
131
+ else:
132
+ error_message = "The current file is neither a list nor a dictionary."
133
+ print_api(error_message, logger_method="critical", logger=logger)
134
+ raise TypeError(error_message)
135
+
136
+ # Write the data back to the file
137
+ jsons.write_json_file(
138
+ final_json_list_of_dicts, record_file_path, indent=2,
139
+ enable_long_file_path=True, **{'logger': logger})
140
+
141
+ logger.info(f"Recorded to file: {record_file_path}")
142
+
143
+ # Indicate task completion
144
+ message_queue.task_done()
@@ -7,7 +7,7 @@ from atomicshop.mitm.message import ClientMessage
7
7
  class RecorderGeneral(RecorderParent):
8
8
  """The class that is responsible for Recording Requests / Responses"""
9
9
  # When initializing main classes through "super" you need to pass parameters to init
10
- def __init__(self, class_client_message: ClientMessage, record_path):
11
- super().__init__(class_client_message, record_path)
10
+ def __init__(self, record_path):
11
+ super().__init__(record_path)
12
12
 
13
13
  self.logger = create_custom_logger()
@@ -8,15 +8,18 @@ from ..basics import dicts
8
8
  class ClientMessage:
9
9
  """ A class that will store all the message details from the client """
10
10
  def __init__(self):
11
- self.request_raw_bytes: bytearray = bytearray()
12
11
  # noinspection PyTypeChecker
13
- self.request_time_received: datetime = None
14
- self.request_raw_decoded: Union[http_parse.HTTPRequestParse, any] = None
15
- self.request_body_parsed = None
12
+ self.timestamp: datetime = None
13
+ # noinspection PyTypeChecker
14
+ self.request_raw_bytes: bytes = None
15
+ self.request_auto_parsed: Union[http_parse.HTTPRequestParse, any] = None
16
+ self.request_custom_parsed: any = None
16
17
  self.request_raw_hex: hex = None
17
- self.response_list_of_raw_bytes: list = list()
18
- self.response_list_of_raw_decoded: list = list()
19
- self.response_list_of_raw_hex: list = list()
18
+ # noinspection PyTypeChecker
19
+ self.response_raw_bytes: bytes = None
20
+ self.response_auto_parsed: any = None
21
+ self.response_custom_parsed: any = None
22
+ self.response_raw_hex: hex = None
20
23
  self.server_name: str = str()
21
24
  self.server_ip: str = str()
22
25
  self.client_ip: str = str()
@@ -25,26 +28,50 @@ class ClientMessage:
25
28
  self.process_name: str = str()
26
29
  self.thread_id = None
27
30
  self.info: str = str()
28
- self.error: str = str()
31
+ self.errors: list = list()
29
32
  self.protocol: str = str()
33
+ self.protocol2: str = str()
34
+ self.protocol3: str = str()
30
35
  self.recorded_file_path: str = str()
36
+ self.action: str = str()
37
+
38
+ def reinitialize_dynamic_vars(self):
39
+ """
40
+ Reinitialize the dynamic variables of the class for the new cycle.
41
+ """
42
+ self.request_raw_bytes = None
43
+ self.timestamp = None
44
+ self.request_auto_parsed = None
45
+ self.request_custom_parsed = None
46
+ self.request_raw_hex = None
47
+ self.response_raw_bytes = None
48
+ self.response_auto_parsed = None
49
+ self.response_custom_parsed = None
50
+ self.response_raw_hex = None
51
+ self.action = None
52
+ self.info = str()
53
+ self.errors = list()
54
+ self.protocol = str()
55
+ self.protocol2 = str()
56
+ self.protocol3 = str()
57
+ self.recorded_file_path = str()
31
58
 
32
59
  def __iter__(self):
33
60
  # __dict__ returns a dictionary containing the instance's attributes
34
61
  for key, value in self.__dict__.items():
35
62
  if key == 'request_raw_bytes':
36
63
  value = str(value)
37
- elif key == 'request_time_received':
64
+ elif key == 'timestamp':
38
65
  value = value.strftime('%Y-%m-%d-%H:%M:%S.%f')
39
- elif key == 'request_raw_decoded':
66
+ elif key == 'request_auto_parsed':
40
67
  if isinstance(value, http_parse.HTTPRequestParse):
41
68
  value = dicts.convert_complex_object_to_dict(value)
42
69
  else:
43
70
  value = str(value)
44
- elif key == 'request_body_parsed':
71
+ elif key == 'request_custom_parsed':
72
+ value = dicts.convert_complex_object_to_dict(value)
73
+ elif key == 'response_raw_bytes':
74
+ value = str(value)
75
+ elif key == 'response_auto_parsed':
45
76
  value = dicts.convert_complex_object_to_dict(value)
46
- elif key == 'response_list_of_raw_bytes':
47
- value = [str(bytes_response) for bytes_response in value]
48
- elif key == 'response_list_of_raw_decoded':
49
- value = [dicts.convert_complex_object_to_dict(complex_response) for complex_response in value]
50
77
  yield key, value
@@ -272,7 +272,7 @@ def mitm_server(config_file_path: str):
272
272
  time.sleep(1)
273
273
  return 1
274
274
 
275
- dns_thread = threading.Thread(target=dns_server_instance.start)
275
+ dns_thread = threading.Thread(target=dns_server_instance.start, name="dns_server")
276
276
  dns_thread.daemon = True
277
277
  dns_thread.start()
278
278
 
@@ -376,7 +376,8 @@ def mitm_server(config_file_path: str):
376
376
  kwargs={
377
377
  'reference_function_name': thread_worker_main,
378
378
  'reference_function_args': (network_logger, statistics_writer, engines_list, reference_module,)
379
- }
379
+ },
380
+ name="accepting_loop"
380
381
  )
381
382
 
382
383
  socket_thread.daemon = True
@@ -7,7 +7,8 @@ from .. import filesystem
7
7
  from .. wrappers.loggingw import consts
8
8
 
9
9
 
10
- REC_FILE_DATE_TIME_FORMAT: str = f'{consts.DEFAULT_ROTATING_SUFFIXES_FROM_WHEN["S"]}_%f'
10
+ REC_FILE_DATE_TIME_MILLISECONDS_FORMAT: str = f'{consts.DEFAULT_ROTATING_SUFFIXES_FROM_WHEN["S"]}_%f'
11
+ REC_FILE_DATE_TIME_FORMAT: str = f'{consts.DEFAULT_ROTATING_SUFFIXES_FROM_WHEN["S"]}'
11
12
  REC_FILE_DATE_FORMAT: str = REC_FILE_DATE_TIME_FORMAT.split('_')[0]
12
13
 
13
14
 
@@ -111,10 +111,18 @@ class SystemResourceMonitor:
111
111
  # The shared results' dictionary.
112
112
  self.results: dict = {}
113
113
 
114
- def start(self, print_kwargs: dict = None):
114
+ def start(
115
+ self,
116
+ print_kwargs: dict = None,
117
+ thread_as_daemon: bool = True
118
+ ):
115
119
  """
116
120
  Start the monitoring process.
117
121
  :param print_kwargs:
122
+ :param thread_as_daemon: bool, set the thread as daemon. If you're running the monitoring process in the main
123
+ process, set it to True. If you're running the monitoring process in a separate process, set it to False.
124
+ In child processes created by multiprocessing.Process, the thread works differently. You might not
125
+ get the desired result.
118
126
  :return:
119
127
  """
120
128
 
@@ -127,7 +135,7 @@ class SystemResourceMonitor:
127
135
  self.interval, self.get_cpu, self.get_memory, self.get_disk_io_bytes, self.get_disk_files_count,
128
136
  self.get_disk_busy_time, self.get_disk_used_percent, self.calculate_maximum_changed_disk_io,
129
137
  self.maximum_disk_io, self.queue_list, self.manager_dict))
130
- self.thread.daemon = True
138
+ self.thread.daemon = thread_as_daemon
131
139
  self.thread.start()
132
140
  else:
133
141
  print_api.print_api("Monitoring is already running.", color='yellow', **print_kwargs)
@@ -205,6 +213,7 @@ def start_monitoring(
205
213
  calculate_maximum_changed_disk_io: bool = False,
206
214
  queue_list: list = None,
207
215
  manager_dict: multiprocessing.managers.DictProxy = None, # multiprocessing.Manager().dict()
216
+ get_results_thread_as_daemon: bool = True,
208
217
  print_kwargs: dict = None
209
218
  ):
210
219
  """
@@ -235,6 +244,10 @@ def start_monitoring(
235
244
  multiprocessing.Process(
236
245
  target=system_resource_monitor.start_monitoring, kwargs={'manager_dict': shared_dict}).start()
237
246
 
247
+ :param get_results_thread_as_daemon: bool, set the thread as daemon. If you're running the monitoring process in the
248
+ main process, set it to True. If you're running the monitoring process in a separate process, set it to False.
249
+ In child processes created by multiprocessing.Process, the thread works differently.
250
+ You might not get the desired result.
238
251
  :param print_kwargs: dict, print kwargs.
239
252
  :return:
240
253
  """
@@ -257,7 +270,7 @@ def start_monitoring(
257
270
  queue_list=queue_list,
258
271
  manager_dict=manager_dict
259
272
  )
260
- SYSTEM_RESOURCES_MONITOR.start()
273
+ SYSTEM_RESOURCES_MONITOR.start(thread_as_daemon=get_results_thread_as_daemon)
261
274
  else:
262
275
  print_api.print_api("System resources monitoring is already running.", color='yellow', **(print_kwargs or {}))
263
276
 
@@ -70,3 +70,20 @@ def get_default_ip_address() -> str:
70
70
  :return: string.
71
71
  """
72
72
  return socket.gethostbyname(socket.gethostname())
73
+
74
+
75
+ def is_socket_closed(socket_object) -> bool:
76
+ """
77
+ Check if the socket is closed.
78
+ :param socket_object: socket object or ssl socket object.
79
+ :return: bool.
80
+ """
81
+ try:
82
+ # If the socket is closed, the fileno() method will raise an exception or return -1.
83
+
84
+ if socket_object.fileno() == -1:
85
+ return True
86
+ else:
87
+ return False
88
+ except socket.error:
89
+ return False
@@ -21,21 +21,25 @@ def peek_first_bytes(client_socket, bytes_amount: int = 1) -> bytes:
21
21
  return client_socket.recv(bytes_amount, socket.MSG_PEEK)
22
22
 
23
23
 
24
- def is_socket_ready_for_read(client_socket, timeout: int = 0) -> bool:
24
+ def is_socket_ready_for_read(socket_instance, timeout: int = 0) -> bool:
25
25
  """
26
26
  Check if socket is ready for read.
27
27
 
28
- :param client_socket: Socket object.
28
+ :param socket_instance: Socket object.
29
29
  :param timeout: Timeout in seconds. The default is no timeout.
30
30
 
31
31
  :return: True if socket is ready for read, False otherwise.
32
32
  """
33
33
 
34
+ # Check if the socket is closed.
35
+ if socket_instance.fileno() == -1:
36
+ return False
37
+
34
38
  # Use select to check if the socket is ready for reading.
35
39
  # 'readable' returns a list of sockets that are ready for reading.
36
40
  # Since we use only one socket, it will return a list with one element if the socket is ready for reading,
37
41
  # or an empty list if the socket is not ready for reading.
38
- readable, _, _ = select.select([client_socket], [], [], timeout)
42
+ readable, _, _ = select.select([socket_instance], [], [], timeout)
39
43
  return bool(readable)
40
44
 
41
45
 
@@ -69,129 +73,115 @@ class Receiver:
69
73
  self.logger: logging.Logger = logger
70
74
 
71
75
  # Function to receive only the buffer, with error handling
72
- def socket_receive_message_buffer(self):
76
+ def chunk_from_buffer(self) -> tuple[bytes, str]:
77
+ """
78
+ Receive a chunk from the socket buffer.
79
+
80
+ :return: Tuple(received chunk binary bytes data, error message string).
81
+ """
73
82
  # Defining the data variable
74
- class_data: bytes = bytes()
83
+ # noinspection PyTypeChecker
84
+ received_data: bytes = None
85
+ # noinspection PyTypeChecker
86
+ error_message: str = None
75
87
 
88
+ # All excepts will be treated as empty message, indicate that socket was closed and will be handled properly.
76
89
  try:
77
90
  # "recv(byte buffer size)" to read the server's response.
78
- class_data = self.ssl_socket.recv(self.buffer_size_receive)
91
+ # A signal to close connection will be empty bytes string: b''.
92
+ received_data = self.ssl_socket.recv(self.buffer_size_receive)
79
93
  except ConnectionAbortedError:
80
- message = "* Connection was aborted by the client. Exiting..."
81
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
82
- # This will be treated as empty message - indicate that socket was closed and will be handled properly.
83
- pass
94
+ error_message = "* Connection was aborted by the client. Exiting..."
95
+ print_api(error_message, logger=self.logger, logger_method='critical', traceback_string=True)
84
96
  except ConnectionResetError:
85
- message = "* Connection was forcibly closed by the client. Exiting..."
86
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
87
- # This will be treated as empty message - indicate that socket was closed and will be handled properly.
88
- pass
97
+ error_message = "* Connection was forcibly closed by the client. Exiting..."
98
+ print_api(error_message, logger=self.logger, logger_method='critical', traceback_string=True)
89
99
  except ssl.SSLError:
90
- message = "* Encountered SSL error on packet receive. Exiting..."
91
- print_api(message, logger=self.logger, logger_method='critical', traceback_string=True)
92
- # This will be treated as empty message - indicate that socket was closed and will be handled properly.
93
- pass
100
+ error_message = "* Encountered SSL error on packet receive. Exiting..."
101
+ print_api(error_message, logger=self.logger, logger_method='critical', traceback_string=True)
94
102
 
95
- if not class_data:
103
+ if received_data == b'':
96
104
  self.logger.info("Empty message received, socket closed on the other side.")
97
105
 
98
- return class_data
106
+ return received_data, error_message
99
107
 
100
- # Function to receive message
101
- def socket_receive_message_full(self):
102
- # Setting timeout for the client to receive connections, since there is no way to know when the message has
103
- # ended and the message is longer than the receiving buffer.
104
- # So we need to set the timeout for the client socket.
105
- # If you set "timeout" on the listening main socket, it is not inherited to the child socket when client
106
- # connected, so you need to set it for the new socket as well.
107
- # Each function need to set the timeout independently
108
- self.ssl_socket.settimeout(self.socket_timeout)
109
- # variable that is responsible to retry over the same receive session if packet is less than buffer
110
- partial_data_received: bool = False
108
+ def socket_receive_message_full(self) -> tuple[bytes, bool, str]:
109
+ """
110
+ Receive the full message from the socket.
111
111
 
112
+ :return: Tuple(full data binary bytes, is socket closed boolean, error message string).
113
+ """
112
114
  # Define the variable that is going to aggregate the whole data received
113
- class_data: bytearray = bytearray()
114
- # Define the variable that will be responsible for receive buffer
115
- class_data_received: bytes = bytes()
115
+ full_data: bytes = bytes()
116
+ # noinspection PyTypeChecker
117
+ error_message: str = None
116
118
 
117
119
  # Infinite loop to accept data from the client
120
+ # We'll skip the 'is_socket_ready_for_read' check on the first run, since we want to read the data anyway,
121
+ # to leave the socket in the blocking mode.
122
+ first_run: bool = True
118
123
  while True:
119
- # SocketTimeout creates an exception that we need to handle with try and except.
120
- try:
121
- # The variable needs to be defined before receiving the socket data or else you will get an error
122
- # - variable not defined.
123
- # function_data_received: bytearray = bytearray()
124
- # Receiving data from the socket with "recv" method, while 1024 byte is a buffer size for the data.
125
- # "decode()" method converts byte message to string.
126
- class_data_received: bytes = self.socket_receive_message_buffer()
127
- # If there is no byte data received from the client and also the full message is not empty, then the loop
128
- # needs to break, since it is the last message that was received from the client
129
- except TimeoutError:
130
- if partial_data_received:
131
- self.logger.info(f"Timed out after {self.socket_timeout} seconds - no more packets. "
132
- f"Passing current request of total {len(class_data)} bytes down the network chain")
133
- # Pass the exception
134
- pass
135
- # Break the while loop, since we already have the request
124
+ # Check if there is data to be read from the socket.
125
+ is_there_data: bool = is_socket_ready_for_read(self.ssl_socket, timeout=0)
126
+ # noinspection PyTypeChecker
127
+ if is_there_data or first_run:
128
+ first_run = False
129
+ # Receive the data from the socket.
130
+ received_chunk, error_message = self.chunk_from_buffer()
131
+ received_chunk: bytes
132
+ error_message: str
133
+
134
+ # And if the message received is not empty then aggregate it to the main "data received" variable
135
+ if received_chunk != b'' and received_chunk is not None:
136
+ full_data += received_chunk
137
+
138
+ self.logger.info(f"Received packet bytes: [{len(received_chunk)}] | "
139
+ f"Total aggregated bytes: [{len(full_data)}]")
140
+
141
+ elif received_chunk == b'' or received_chunk is None:
142
+ # If there received_chunk is None, this means that the socket was closed,
143
+ # since it is a connection error.
144
+ # Same goes for the empty message.
145
+ is_socket_closed = True
136
146
  break
137
- else:
138
- self.logger.info(
139
- # Dividing number of seconds by 60 to get minutes
140
- f"{self.socket_timeout/60} minutes timeout reached on 'socket.recv()' - no data received from "
141
- f"{self.class_client_address}:{self.class_client_local_port}. Still waiting...")
142
- # Pass the exception
143
- pass
144
- # Changing the socket timeout back to none since receiving operation has been finished.
145
- self.ssl_socket.settimeout(None)
146
- # Continue to the next iteration inside while
147
- continue
148
-
149
- # And if the message received is not empty then aggregate it to the main "data received" variable
150
- if class_data_received:
151
- class_data.extend(class_data_received)
152
-
153
- self.logger.info(f"Received packet with {len(class_data_received)} bytes. "
154
- f"Current aggregated request of total: {len(class_data)} bytes")
155
-
156
- # If the first received session is less than the buffer size, then the full message was received
157
- if len(class_data_received) < self.buffer_size_receive:
158
- # Since we already received some data from the other side, the retried variable will be true
159
- partial_data_received = True
160
- self.logger.info(f"Receiving the buffer again...")
161
-
162
- # In this case the socket timeout will be 2 seconds to wait for more packets.
163
- # If there are no more packets, receiver will end its activity and pass the message to
164
- # the rest of the components in the network chain
165
- if self.socket_timeout != 0.5:
166
- self.socket_timeout = 0.5
167
- self.ssl_socket.settimeout(self.socket_timeout)
168
- self.logger.info(f"Timeout changed to {self.socket_timeout} seconds")
169
-
170
- # Continue to the next receive on the socket
171
- continue
172
147
  else:
173
- if class_data:
174
- self.logger.info(f"Since there's request received from the client of total {len(class_data)} "
175
- f"bytes, we'll first process it, and when the receiver will ask for data from "
176
- f"client in the next cycle - empty message will be received again, and current "
177
- f"socket will be finally closed.")
148
+ # If there is no data to be read from the socket, it doesn't mean that the socket is closed.
149
+ is_socket_closed = False
150
+ received_chunk = None
178
151
  break
179
152
 
180
- return class_data
153
+ if full_data:
154
+ self.logger.info(f"Received total: [{len(full_data)}] bytes")
155
+
156
+ # In case the full data is empty, and the received chunk is None, it doesn't mean that the socket is closed.
157
+ # But it means that there was no data to be read from the socket, because of error or timeout.
158
+ if full_data == b'' and received_chunk is None:
159
+ full_data = None
160
+
161
+ return full_data, is_socket_closed, error_message
162
+
163
+ def receive(self) -> tuple[bytes, bool, str]:
164
+ """
165
+ Receive the message from the socket.
181
166
 
182
- # noinspection PyBroadException
183
- def receive(self):
184
- # Getting client address from the socket
167
+ :return: Tuple(
168
+ data binary bytes,
169
+ is socket closed boolean,
170
+ error message string if there was a connection exception).
171
+ """
172
+ # Getting client address and Local port from the socket
185
173
  self.class_client_address = self.ssl_socket.getpeername()[0]
186
- # Getting client Local port from the socket
187
174
  self.class_client_local_port = self.ssl_socket.getpeername()[1]
188
175
 
189
176
  # Receiving data from the socket and closing the socket if send is finished.
190
177
  self.logger.info(f"Waiting for data from {self.class_client_address}:{self.class_client_local_port}")
191
- function_client_data: bytearray = self.socket_receive_message_full()
178
+ socket_data_bytes, is_socket_closed, error_message = self.socket_receive_message_full()
179
+ socket_data_bytes: bytes
180
+ is_socket_closed: bool
181
+ error_message: str
192
182
 
193
- if function_client_data:
183
+ if socket_data_bytes:
194
184
  # Put only 100 characters to the log, since we record the message any way in full - later.
195
- self.logger.info(f"Received: {function_client_data[0: 100]}...")
185
+ self.logger.info(f"Received: {socket_data_bytes[0: 100]}...")
196
186
 
197
- return function_client_data
187
+ return socket_data_bytes, is_socket_closed, error_message