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.
- atomicshop/__init__.py +1 -1
- atomicshop/basics/ansi_escape_codes.py +3 -1
- atomicshop/file_io/docxs.py +26 -17
- atomicshop/http_parse.py +118 -77
- atomicshop/mitm/config_static.py +1 -1
- atomicshop/mitm/connection_thread_worker.py +387 -257
- atomicshop/mitm/engines/__parent/parser___parent.py +2 -2
- atomicshop/mitm/engines/__parent/recorder___parent.py +83 -24
- atomicshop/mitm/engines/__reference_general/recorder___reference_general.py +2 -2
- atomicshop/mitm/message.py +42 -15
- atomicshop/mitm/mitm_main.py +3 -2
- atomicshop/mitm/recs_files.py +2 -1
- atomicshop/system_resource_monitor.py +16 -3
- atomicshop/wrappers/socketw/base.py +17 -0
- atomicshop/wrappers/socketw/receiver.py +90 -100
- atomicshop/wrappers/socketw/sender.py +5 -8
- atomicshop/wrappers/socketw/sni.py +0 -1
- atomicshop/wrappers/socketw/socket_client.py +2 -2
- atomicshop/wrappers/socketw/statistics_csv.py +15 -6
- {atomicshop-2.17.3.dist-info → atomicshop-2.18.1.dist-info}/METADATA +1 -1
- {atomicshop-2.17.3.dist-info → atomicshop-2.18.1.dist-info}/RECORD +24 -24
- {atomicshop-2.17.3.dist-info → atomicshop-2.18.1.dist-info}/LICENSE.txt +0 -0
- {atomicshop-2.17.3.dist-info → atomicshop-2.18.1.dist-info}/WHEEL +0 -0
- {atomicshop-2.17.3.dist-info → atomicshop-2.18.1.dist-info}/top_level.txt +0 -0
|
@@ -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.
|
|
13
|
+
self.class_client_message.request_custom_parsed = b''
|
|
14
14
|
|
|
15
|
-
self.logger.info(f"Parsed: {self.class_client_message.
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
#
|
|
80
|
-
|
|
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,
|
|
11
|
-
super().__init__(
|
|
10
|
+
def __init__(self, record_path):
|
|
11
|
+
super().__init__(record_path)
|
|
12
12
|
|
|
13
13
|
self.logger = create_custom_logger()
|
atomicshop/mitm/message.py
CHANGED
|
@@ -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.
|
|
14
|
-
|
|
15
|
-
self.
|
|
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
|
-
|
|
18
|
-
self.
|
|
19
|
-
self.
|
|
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.
|
|
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 == '
|
|
64
|
+
elif key == 'timestamp':
|
|
38
65
|
value = value.strftime('%Y-%m-%d-%H:%M:%S.%f')
|
|
39
|
-
elif key == '
|
|
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 == '
|
|
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
|
atomicshop/mitm/mitm_main.py
CHANGED
|
@@ -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
|
atomicshop/mitm/recs_files.py
CHANGED
|
@@ -7,7 +7,8 @@ from .. import filesystem
|
|
|
7
7
|
from .. wrappers.loggingw import consts
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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([
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
print_api(
|
|
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
|
-
|
|
86
|
-
print_api(
|
|
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
|
-
|
|
91
|
-
print_api(
|
|
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
|
|
103
|
+
if received_data == b'':
|
|
96
104
|
self.logger.info("Empty message received, socket closed on the other side.")
|
|
97
105
|
|
|
98
|
-
return
|
|
106
|
+
return received_data, error_message
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
114
|
-
#
|
|
115
|
-
|
|
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
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
185
|
+
self.logger.info(f"Received: {socket_data_bytes[0: 100]}...")
|
|
196
186
|
|
|
197
|
-
return
|
|
187
|
+
return socket_data_bytes, is_socket_closed, error_message
|