python-snap7 3.2.0__py3-none-any.whl → 3.2.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.
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/METADATA +5 -1
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/RECORD +12 -12
- snap7/async_client.py +76 -66
- snap7/client.py +82 -89
- snap7/client_base.py +75 -2
- snap7/s7protocol.py +170 -134
- snap7/server/__init__.py +178 -220
- snap7/util/getters.py +5 -5
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/WHEEL +0 -0
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/entry_points.txt +0 -0
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/licenses/LICENSE +0 -0
- {python_snap7-3.2.0.dist-info → python_snap7-3.2.1.dist-info}/top_level.txt +0 -0
snap7/server/__init__.py
CHANGED
|
@@ -94,6 +94,8 @@ class Server:
|
|
|
94
94
|
# Memory areas
|
|
95
95
|
self.memory_areas: Dict[Tuple[S7Area, int], bytearray] = {}
|
|
96
96
|
self.area_locks: Dict[Tuple[S7Area, int], threading.Lock] = {}
|
|
97
|
+
self._upload_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
|
|
98
|
+
self._download_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
|
|
97
99
|
|
|
98
100
|
# Protocol handler
|
|
99
101
|
self.protocol = S7Protocol()
|
|
@@ -733,6 +735,10 @@ class Server:
|
|
|
733
735
|
# Send response
|
|
734
736
|
if response_data:
|
|
735
737
|
connection.send_data(response_data)
|
|
738
|
+
context = self._download_contexts.get(address)
|
|
739
|
+
if context is not None and context["phase"] == "start_ack":
|
|
740
|
+
context["phase"] = "awaiting_fragment"
|
|
741
|
+
connection.send_data(self._build_download_service_request(context, S7Function.DOWNLOAD_BLOCK))
|
|
736
742
|
|
|
737
743
|
except socket.timeout:
|
|
738
744
|
continue
|
|
@@ -758,10 +764,8 @@ class Server:
|
|
|
758
764
|
self.clients.remove(current_thread)
|
|
759
765
|
self.client_count = max(0, self.client_count - 1)
|
|
760
766
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if hasattr(self, "_upload_contexts"):
|
|
764
|
-
self._upload_contexts.pop(address, None)
|
|
767
|
+
self._download_contexts.pop(address, None)
|
|
768
|
+
self._upload_contexts.pop(address, None)
|
|
765
769
|
|
|
766
770
|
self._emit_event(EVC_CLIENT_DISCONNECTED, sender=self._event_sender(address))
|
|
767
771
|
logger.info(f"Client {address} handler finished")
|
|
@@ -778,9 +782,14 @@ class Server:
|
|
|
778
782
|
Response PDU data or None
|
|
779
783
|
"""
|
|
780
784
|
try:
|
|
785
|
+
if len(request_data) >= 2 and request_data[1] == S7PDUType.ACK_DATA:
|
|
786
|
+
return self._handle_download_response(request_data, client_address)
|
|
781
787
|
# Parse S7 request
|
|
782
788
|
request = self._parse_request(request_data)
|
|
783
789
|
|
|
790
|
+
if client_address in self._download_contexts:
|
|
791
|
+
return self._build_error_response(request, 0x8104)
|
|
792
|
+
|
|
784
793
|
# Check PDU type first
|
|
785
794
|
pdu_type = request.get("pdu_type", S7PDUType.REQUEST)
|
|
786
795
|
|
|
@@ -815,9 +824,9 @@ class Server:
|
|
|
815
824
|
elif function_code == S7Function.REQUEST_DOWNLOAD:
|
|
816
825
|
return self._handle_request_download(request, client_address)
|
|
817
826
|
elif function_code == S7Function.DOWNLOAD_BLOCK:
|
|
818
|
-
return self.
|
|
827
|
+
return self._build_error_response(request, 0x8104)
|
|
819
828
|
elif function_code == S7Function.DOWNLOAD_ENDED:
|
|
820
|
-
return self.
|
|
829
|
+
return self._build_error_response(request, 0x8104)
|
|
821
830
|
else:
|
|
822
831
|
logger.warning(f"Unsupported function code: {function_code}")
|
|
823
832
|
return self._build_error_response(request, 0x8001) # Function not supported
|
|
@@ -1335,6 +1344,10 @@ class Server:
|
|
|
1335
1344
|
|
|
1336
1345
|
data_section = pdu[offset : offset + data_len]
|
|
1337
1346
|
request["data"] = self._parse_data_section(data_section)
|
|
1347
|
+
offset += data_len
|
|
1348
|
+
|
|
1349
|
+
if offset != len(pdu):
|
|
1350
|
+
raise S7ProtocolError("S7 request contains trailing bytes")
|
|
1338
1351
|
|
|
1339
1352
|
return request
|
|
1340
1353
|
|
|
@@ -1898,23 +1911,34 @@ class Server:
|
|
|
1898
1911
|
data_section = request.get("data", {})
|
|
1899
1912
|
raw_data = data_section.get("data", b"")
|
|
1900
1913
|
|
|
1901
|
-
if len(raw_data)
|
|
1902
|
-
|
|
1903
|
-
def from_bcd(value: int) -> int:
|
|
1904
|
-
return ((value >> 4) * 10) + (value & 0x0F)
|
|
1914
|
+
if len(raw_data) != 10 or raw_data[:2] != b"\x00\x19":
|
|
1915
|
+
return self._build_userdata_error_response(request, 0x8104)
|
|
1905
1916
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
minute = from_bcd(raw_data[5])
|
|
1911
|
-
second = from_bcd(raw_data[6])
|
|
1917
|
+
def from_bcd(value: int) -> int:
|
|
1918
|
+
if value >> 4 > 9 or value & 0x0F > 9:
|
|
1919
|
+
raise ValueError("Invalid BCD digit")
|
|
1920
|
+
return ((value >> 4) * 10) + (value & 0x0F)
|
|
1912
1921
|
|
|
1913
|
-
|
|
1914
|
-
|
|
1922
|
+
try:
|
|
1923
|
+
from datetime import datetime
|
|
1924
|
+
|
|
1925
|
+
millisecond = from_bcd(raw_data[8]) * 10 + (raw_data[9] >> 4)
|
|
1926
|
+
if millisecond > 999 or not 1 <= (raw_data[9] & 0x0F) <= 7:
|
|
1927
|
+
raise ValueError("Invalid clock millisecond or weekday")
|
|
1928
|
+
year = from_bcd(raw_data[2])
|
|
1929
|
+
timestamp = datetime(
|
|
1930
|
+
2000 + year if year < 90 else 1900 + year,
|
|
1931
|
+
from_bcd(raw_data[3]),
|
|
1932
|
+
from_bcd(raw_data[4]),
|
|
1933
|
+
from_bcd(raw_data[5]),
|
|
1934
|
+
from_bcd(raw_data[6]),
|
|
1935
|
+
from_bcd(raw_data[7]),
|
|
1936
|
+
millisecond * 1000,
|
|
1915
1937
|
)
|
|
1916
|
-
|
|
1917
|
-
|
|
1938
|
+
except ValueError:
|
|
1939
|
+
return self._build_userdata_error_response(request, 0x8104)
|
|
1940
|
+
|
|
1941
|
+
logger.info(f"Set clock from {client_address}: {timestamp}")
|
|
1918
1942
|
|
|
1919
1943
|
# Return success (empty response data)
|
|
1920
1944
|
return self._build_userdata_success_response(request, userdata_params, b"")
|
|
@@ -2243,37 +2267,32 @@ class Server:
|
|
|
2243
2267
|
try:
|
|
2244
2268
|
raw_params = request.get("raw_parameters", b"")
|
|
2245
2269
|
|
|
2246
|
-
#
|
|
2247
|
-
#
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2270
|
+
# TReqFunStartUploadParams: function, six reserved bytes,
|
|
2271
|
+
# upload ID, and `_0TNNNNNA` block address.
|
|
2272
|
+
if (
|
|
2273
|
+
len(raw_params) != 18
|
|
2274
|
+
or raw_params[0] != S7Function.START_UPLOAD
|
|
2275
|
+
or raw_params[8] != 9
|
|
2276
|
+
or raw_params[9:11] != b"_0"
|
|
2277
|
+
or raw_params[17:18] != b"A"
|
|
2278
|
+
):
|
|
2279
|
+
return self._build_error_response(request, 0x8104)
|
|
2280
|
+
block_type = raw_params[11]
|
|
2281
|
+
try:
|
|
2282
|
+
block_num = int(raw_params[12:17].decode("ascii"))
|
|
2283
|
+
except (UnicodeDecodeError, ValueError):
|
|
2284
|
+
return self._build_error_response(request, 0x8104)
|
|
2261
2285
|
|
|
2262
2286
|
logger.info(f"Start upload request from {client_address}: type={block_type:#02x}, num={block_num}")
|
|
2263
2287
|
|
|
2264
2288
|
# Generate upload ID and get block length
|
|
2265
2289
|
upload_id = 1 # Simple upload ID
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
area_key = (S7Area.DB, block_num)
|
|
2271
|
-
if area_key in self.memory_areas:
|
|
2272
|
-
block_length = len(self.memory_areas[area_key])
|
|
2290
|
+
area_key = (S7Area.DB, block_num)
|
|
2291
|
+
if block_type != 0x41 or area_key not in self.memory_areas:
|
|
2292
|
+
return self._build_error_response(request, 0xD209)
|
|
2293
|
+
block_length = len(self.memory_areas[area_key]) + 36
|
|
2273
2294
|
|
|
2274
2295
|
# Store upload context for this client
|
|
2275
|
-
if not hasattr(self, "_upload_contexts"):
|
|
2276
|
-
self._upload_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
|
|
2277
2296
|
self._upload_contexts[client_address] = {
|
|
2278
2297
|
"upload_id": upload_id,
|
|
2279
2298
|
"block_type": block_type,
|
|
@@ -2281,19 +2300,10 @@ class Server:
|
|
|
2281
2300
|
"offset": 0,
|
|
2282
2301
|
}
|
|
2283
2302
|
|
|
2284
|
-
#
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
">BBBIB",
|
|
2289
|
-
S7Function.START_UPLOAD,
|
|
2290
|
-
0x00, # Status
|
|
2291
|
-
0x00, # Reserved
|
|
2292
|
-
upload_id,
|
|
2293
|
-
len(block_len_str),
|
|
2294
|
-
)
|
|
2295
|
-
+ block_len_str
|
|
2296
|
-
)
|
|
2303
|
+
# TResFunStartUploadParams: function, six reserved bytes,
|
|
2304
|
+
# upload ID, three reserved bytes, and five ASCII length digits.
|
|
2305
|
+
block_len_str = f"{block_length:05d}".encode("ascii")
|
|
2306
|
+
param_data = bytes((S7Function.START_UPLOAD,)) + bytes(6) + bytes((upload_id,)) + bytes(3) + block_len_str
|
|
2297
2307
|
|
|
2298
2308
|
header = struct.pack(
|
|
2299
2309
|
">BBHHHHBB",
|
|
@@ -2326,11 +2336,14 @@ class Server:
|
|
|
2326
2336
|
"""
|
|
2327
2337
|
try:
|
|
2328
2338
|
# Get upload context for this client
|
|
2329
|
-
if
|
|
2339
|
+
if client_address not in self._upload_contexts:
|
|
2330
2340
|
logger.warning(f"Upload request without start_upload from {client_address}")
|
|
2331
2341
|
return self._build_error_response(request, 0x8104)
|
|
2332
2342
|
|
|
2333
2343
|
ctx = self._upload_contexts[client_address]
|
|
2344
|
+
raw_params = request.get("raw_parameters", b"")
|
|
2345
|
+
if len(raw_params) != 8 or raw_params[0] != S7Function.UPLOAD or raw_params[7] != ctx["upload_id"]:
|
|
2346
|
+
return self._build_error_response(request, 0x8104)
|
|
2334
2347
|
block_type = ctx["block_type"]
|
|
2335
2348
|
block_num = ctx["block_num"]
|
|
2336
2349
|
|
|
@@ -2342,20 +2355,33 @@ class Server:
|
|
|
2342
2355
|
with self.area_locks[area_key]:
|
|
2343
2356
|
block_data = bytes(self.memory_areas[area_key])
|
|
2344
2357
|
|
|
2345
|
-
|
|
2358
|
+
offset = ctx["offset"]
|
|
2359
|
+
first_fragment = offset == 0
|
|
2360
|
+
compact_header = b""
|
|
2361
|
+
if first_fragment:
|
|
2362
|
+
compact_header_data = bytearray(36)
|
|
2363
|
+
compact_header_data[2] = 0x01
|
|
2364
|
+
compact_header_data[4] = 0x05 # S7 language: DB
|
|
2365
|
+
compact_header_data[5] = 0x0A # S7 sub-block type: DB
|
|
2366
|
+
struct.pack_into(">H", compact_header_data, 6, block_num)
|
|
2367
|
+
struct.pack_into(">I", compact_header_data, 8, len(block_data) + 36)
|
|
2368
|
+
struct.pack_into(">H", compact_header_data, 34, len(block_data))
|
|
2369
|
+
compact_header = bytes(compact_header_data)
|
|
2370
|
+
|
|
2371
|
+
chunk_capacity = 462 - len(compact_header)
|
|
2372
|
+
chunk = block_data[offset : offset + chunk_capacity]
|
|
2373
|
+
ctx["offset"] += len(chunk)
|
|
2374
|
+
is_last = ctx["offset"] >= len(block_data)
|
|
2375
|
+
|
|
2376
|
+
logger.info(f"Upload request from {client_address}: sending {len(chunk)} bytes")
|
|
2346
2377
|
|
|
2347
2378
|
# Build response with data
|
|
2348
|
-
#
|
|
2349
|
-
param_data =
|
|
2350
|
-
">BBBI",
|
|
2351
|
-
S7Function.UPLOAD,
|
|
2352
|
-
0x01, # Status: last packet
|
|
2353
|
-
0x00, # Reserved
|
|
2354
|
-
ctx["upload_id"],
|
|
2355
|
-
)
|
|
2379
|
+
# EoU: 0x00 = end of upload, 0x01 = upload in progress.
|
|
2380
|
+
param_data = bytes((S7Function.UPLOAD, 0x00 if is_last else 0x01))
|
|
2356
2381
|
|
|
2357
|
-
#
|
|
2358
|
-
|
|
2382
|
+
# The first fragment starts with the 36-byte compact block header.
|
|
2383
|
+
payload = compact_header + chunk
|
|
2384
|
+
data_section = struct.pack(">HH", len(payload), 0x00FB) + payload
|
|
2359
2385
|
|
|
2360
2386
|
header = struct.pack(
|
|
2361
2387
|
">BBHHHHBB",
|
|
@@ -2387,9 +2413,18 @@ class Server:
|
|
|
2387
2413
|
Response PDU acknowledging end of upload
|
|
2388
2414
|
"""
|
|
2389
2415
|
try:
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2416
|
+
if client_address not in self._upload_contexts:
|
|
2417
|
+
return self._build_error_response(request, 0x8104)
|
|
2418
|
+
ctx = self._upload_contexts[client_address]
|
|
2419
|
+
raw_params = request.get("raw_parameters", b"")
|
|
2420
|
+
if (
|
|
2421
|
+
len(raw_params) != 8
|
|
2422
|
+
or raw_params[0] != S7Function.END_UPLOAD
|
|
2423
|
+
or raw_params[7] != ctx["upload_id"]
|
|
2424
|
+
or ctx["offset"] < len(self.memory_areas[(S7Area.DB, ctx["block_num"])])
|
|
2425
|
+
):
|
|
2426
|
+
return self._build_error_response(request, 0x8104)
|
|
2427
|
+
self._upload_contexts.pop(client_address, None)
|
|
2393
2428
|
|
|
2394
2429
|
logger.info(f"End upload from {client_address}")
|
|
2395
2430
|
|
|
@@ -2428,20 +2463,22 @@ class Server:
|
|
|
2428
2463
|
try:
|
|
2429
2464
|
raw_params = request.get("raw_parameters", b"")
|
|
2430
2465
|
|
|
2431
|
-
|
|
2432
|
-
|
|
2466
|
+
if (
|
|
2467
|
+
len(raw_params) != 32
|
|
2468
|
+
or raw_params[:3] != bytes((S7Function.REQUEST_DOWNLOAD, 0, 1))
|
|
2469
|
+
or raw_params[3:8] != bytes(5)
|
|
2470
|
+
or raw_params[8:11] != b"\x09_0"
|
|
2471
|
+
or raw_params[17:20] != b"P\x0d1"
|
|
2472
|
+
or request.get("data_length") != 0
|
|
2473
|
+
or client_address in self._download_contexts
|
|
2474
|
+
):
|
|
2433
2475
|
return self._build_error_response(request, 0x8104)
|
|
2434
|
-
|
|
2435
|
-
addr_len = raw_params[4]
|
|
2436
|
-
address_end = 5 + addr_len
|
|
2437
|
-
if addr_len < 7 or len(raw_params) < address_end:
|
|
2438
|
-
return self._build_error_response(request, 0x8104)
|
|
2439
|
-
|
|
2440
|
-
block_addr = raw_params[5:address_end]
|
|
2441
2476
|
try:
|
|
2442
|
-
block_type =
|
|
2443
|
-
block_num = int(
|
|
2444
|
-
|
|
2477
|
+
block_type = raw_params[11]
|
|
2478
|
+
block_num = int(raw_params[12:17].decode("ascii"))
|
|
2479
|
+
declared_size = int(raw_params[20:26].decode("ascii"))
|
|
2480
|
+
mc7_size = int(raw_params[26:32].decode("ascii"))
|
|
2481
|
+
except (UnicodeDecodeError, ValueError):
|
|
2445
2482
|
return self._build_error_response(request, 0x8104)
|
|
2446
2483
|
|
|
2447
2484
|
logger.info(f"Request download from {client_address}: type={block_type:#02x}, num={block_num}")
|
|
@@ -2456,35 +2493,20 @@ class Server:
|
|
|
2456
2493
|
|
|
2457
2494
|
area_capacity = len(self.memory_areas[area_key])
|
|
2458
2495
|
|
|
2459
|
-
|
|
2460
|
-
# transfer early, while still enforcing the limit on every chunk.
|
|
2461
|
-
if len(raw_params) <= address_end:
|
|
2462
|
-
return self._build_error_response(request, 0x8104)
|
|
2463
|
-
|
|
2464
|
-
length_len = raw_params[address_end]
|
|
2465
|
-
length_start = address_end + 1
|
|
2466
|
-
length_end = length_start + length_len
|
|
2467
|
-
if not length_len or len(raw_params) < length_end:
|
|
2468
|
-
return self._build_error_response(request, 0x8104)
|
|
2469
|
-
|
|
2470
|
-
try:
|
|
2471
|
-
declared_size = int(raw_params[length_start:length_end])
|
|
2472
|
-
except ValueError:
|
|
2473
|
-
return self._build_error_response(request, 0x8104)
|
|
2474
|
-
if declared_size < 0 or declared_size > area_capacity:
|
|
2496
|
+
if declared_size > area_capacity or mc7_size > declared_size:
|
|
2475
2497
|
logger.warning(
|
|
2476
2498
|
f"Download rejected: declared size {declared_size} outside DB{block_num} capacity 0..{area_capacity}"
|
|
2477
2499
|
)
|
|
2478
2500
|
return self._build_error_response(request, 0x8104)
|
|
2479
2501
|
|
|
2480
2502
|
# Store download context
|
|
2481
|
-
if not hasattr(self, "_download_contexts"):
|
|
2482
|
-
self._download_contexts: Dict[Tuple[str, int], Dict[str, Any]] = {}
|
|
2483
2503
|
self._download_contexts[client_address] = {
|
|
2484
2504
|
"block_type": block_type,
|
|
2485
2505
|
"block_num": block_num,
|
|
2486
2506
|
"data": bytearray(),
|
|
2487
2507
|
"max_size": declared_size,
|
|
2508
|
+
"phase": "start_ack",
|
|
2509
|
+
"sequence": request["sequence"],
|
|
2488
2510
|
}
|
|
2489
2511
|
|
|
2490
2512
|
# Build response acknowledging download
|
|
@@ -2508,125 +2530,61 @@ class Server:
|
|
|
2508
2530
|
logger.error(f"Error handling request download: {e}")
|
|
2509
2531
|
return self._build_error_response(request, 0x8000)
|
|
2510
2532
|
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
param_data = struct.pack(">B", S7Function.DOWNLOAD_BLOCK)
|
|
2548
|
-
|
|
2549
|
-
header = struct.pack(
|
|
2550
|
-
">BBHHHHBB",
|
|
2551
|
-
0x32, # Protocol ID
|
|
2552
|
-
S7PDUType.ACK_DATA, # PDU type
|
|
2553
|
-
0x0000, # Reserved
|
|
2554
|
-
request["sequence"], # Sequence
|
|
2555
|
-
len(param_data), # Parameter length
|
|
2556
|
-
0x0000, # Data length
|
|
2557
|
-
0x00, # Error class (success)
|
|
2558
|
-
0x00, # Error code (success)
|
|
2559
|
-
)
|
|
2560
|
-
|
|
2561
|
-
return header + param_data
|
|
2562
|
-
|
|
2563
|
-
except Exception as e:
|
|
2564
|
-
logger.error(f"Error handling download block: {e}")
|
|
2565
|
-
return self._build_error_response(request, 0x8000)
|
|
2566
|
-
|
|
2567
|
-
def _handle_download_ended(self, request: Dict[str, Any], client_address: Tuple[str, int]) -> bytes:
|
|
2568
|
-
"""
|
|
2569
|
-
Handle download ended - finalize block storage.
|
|
2570
|
-
|
|
2571
|
-
Args:
|
|
2572
|
-
request: Parsed S7 request
|
|
2573
|
-
client_address: Client address for logging
|
|
2574
|
-
|
|
2575
|
-
Returns:
|
|
2576
|
-
Response PDU confirming download complete
|
|
2577
|
-
"""
|
|
2578
|
-
try:
|
|
2579
|
-
# Get download context
|
|
2580
|
-
if not hasattr(self, "_download_contexts") or client_address not in self._download_contexts:
|
|
2581
|
-
logger.warning(f"Download ended without download_block from {client_address}")
|
|
2582
|
-
return self._build_error_response(request, 0x8104)
|
|
2583
|
-
|
|
2584
|
-
ctx = self._download_contexts[client_address]
|
|
2585
|
-
block_type = ctx["block_type"]
|
|
2586
|
-
block_num = ctx["block_num"]
|
|
2587
|
-
block_data = ctx["data"]
|
|
2588
|
-
|
|
2589
|
-
# Store block data — only into pre-registered areas.
|
|
2590
|
-
# Reject downloads to unknown areas to prevent unbounded memory
|
|
2591
|
-
# allocation from attacker-controlled block numbers.
|
|
2592
|
-
if block_type == 0x41: # DB
|
|
2593
|
-
area_key = (S7Area.DB, block_num)
|
|
2594
|
-
if area_key in self.memory_areas:
|
|
2595
|
-
# Update existing area - copy data into existing area without resizing
|
|
2596
|
-
with self.area_locks[area_key]:
|
|
2597
|
-
existing_area = self.memory_areas[area_key]
|
|
2598
|
-
copy_len = min(len(block_data), len(existing_area))
|
|
2599
|
-
existing_area[0:copy_len] = block_data[0:copy_len]
|
|
2600
|
-
else:
|
|
2601
|
-
logger.warning(f"Download rejected: area DB{block_num} not registered")
|
|
2602
|
-
del self._download_contexts[client_address]
|
|
2603
|
-
return self._build_error_response(request, 0x8104)
|
|
2604
|
-
|
|
2605
|
-
logger.info(f"Download ended from {client_address}: stored {len(block_data)} bytes to {block_type:#02x}:{block_num}")
|
|
2606
|
-
|
|
2607
|
-
# Clean up context
|
|
2608
|
-
del self._download_contexts[client_address]
|
|
2609
|
-
|
|
2610
|
-
# Build response
|
|
2611
|
-
param_data = struct.pack(">B", S7Function.DOWNLOAD_ENDED)
|
|
2612
|
-
|
|
2613
|
-
header = struct.pack(
|
|
2614
|
-
">BBHHHHBB",
|
|
2615
|
-
0x32, # Protocol ID
|
|
2616
|
-
S7PDUType.ACK_DATA, # PDU type
|
|
2617
|
-
0x0000, # Reserved
|
|
2618
|
-
request["sequence"], # Sequence
|
|
2619
|
-
len(param_data), # Parameter length
|
|
2620
|
-
0x0000, # Data length
|
|
2621
|
-
0x00, # Error class (success)
|
|
2622
|
-
0x00, # Error code (success)
|
|
2623
|
-
)
|
|
2624
|
-
|
|
2625
|
-
return header + param_data
|
|
2533
|
+
@staticmethod
|
|
2534
|
+
def _build_download_service_request(context: Dict[str, Any], function: int) -> bytes:
|
|
2535
|
+
"""Ask the client for the next block fragment or the final acknowledgement."""
|
|
2536
|
+
context["sequence"] = (context["sequence"] + 1) & 0xFFFF
|
|
2537
|
+
address = b"_0" + bytes((context["block_type"],)) + f"{context['block_num']:05d}".encode("ascii") + b"P"
|
|
2538
|
+
params = bytes((function,)) + bytes(7) + b"\x09" + address
|
|
2539
|
+
return struct.pack(">BBHHHH", 0x32, S7PDUType.REQUEST, 0, context["sequence"], len(params), 0) + params
|
|
2540
|
+
|
|
2541
|
+
def _handle_download_response(self, pdu: bytes, client_address: Tuple[str, int]) -> Optional[bytes]:
|
|
2542
|
+
"""Advance a PLC-driven download after checking the client's ACK_DATA."""
|
|
2543
|
+
context = self._download_contexts.get(client_address)
|
|
2544
|
+
sequence = int.from_bytes(pdu[4:6], "big") if len(pdu) >= 6 else 0
|
|
2545
|
+
request = {"sequence": sequence}
|
|
2546
|
+
|
|
2547
|
+
def reject() -> bytes:
|
|
2548
|
+
self._download_contexts.pop(client_address, None)
|
|
2549
|
+
return self._build_error_response(request, 0x8104)
|
|
2550
|
+
|
|
2551
|
+
if context is None or len(pdu) < 12 or pdu[0:2] != bytes((0x32, S7PDUType.ACK_DATA)):
|
|
2552
|
+
return reject()
|
|
2553
|
+
_, _, _, _, param_len, data_len, error = struct.unpack(">BBHHHHH", pdu[:12])
|
|
2554
|
+
if sequence != context["sequence"] or error != 0 or len(pdu) != 12 + param_len + data_len:
|
|
2555
|
+
return reject()
|
|
2556
|
+
params = pdu[12 : 12 + param_len]
|
|
2557
|
+
data = pdu[12 + param_len :]
|
|
2558
|
+
|
|
2559
|
+
if context["phase"] == "awaiting_end":
|
|
2560
|
+
if params != bytes((S7Function.DOWNLOAD_ENDED,)) or data:
|
|
2561
|
+
return reject()
|
|
2562
|
+
area_key = (S7Area.DB, context["block_num"])
|
|
2563
|
+
if area_key not in self.memory_areas or len(context["data"]) != context["max_size"]:
|
|
2564
|
+
return reject()
|
|
2565
|
+
with self.area_locks[area_key]:
|
|
2566
|
+
self.memory_areas[area_key][: context["max_size"]] = context["data"]
|
|
2567
|
+
self._download_contexts.pop(client_address, None)
|
|
2568
|
+
return None
|
|
2626
2569
|
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2570
|
+
if context["phase"] != "awaiting_fragment" or len(params) != 2 or params[0] != S7Function.DOWNLOAD_BLOCK:
|
|
2571
|
+
return reject()
|
|
2572
|
+
if params[1] not in (0, 1) or len(data) < 4:
|
|
2573
|
+
return reject()
|
|
2574
|
+
length, marker = struct.unpack(">HH", data[:4])
|
|
2575
|
+
payload = data[4:]
|
|
2576
|
+
if marker != 0x00FB or length != len(payload) or len(payload) > 462:
|
|
2577
|
+
return reject()
|
|
2578
|
+
new_size = len(context["data"]) + len(payload)
|
|
2579
|
+
if new_size > context["max_size"] or (params[1] == 0) != (new_size == context["max_size"]):
|
|
2580
|
+
return reject()
|
|
2581
|
+
if not payload and params[1] == 1:
|
|
2582
|
+
return reject()
|
|
2583
|
+
context["data"].extend(payload)
|
|
2584
|
+
if params[1] == 0:
|
|
2585
|
+
context["phase"] = "awaiting_end"
|
|
2586
|
+
return self._build_download_service_request(context, S7Function.DOWNLOAD_ENDED)
|
|
2587
|
+
return self._build_download_service_request(context, S7Function.DOWNLOAD_BLOCK)
|
|
2630
2588
|
|
|
2631
2589
|
def __enter__(self) -> "Server":
|
|
2632
2590
|
"""Context manager entry."""
|
snap7/util/getters.py
CHANGED
|
@@ -32,7 +32,7 @@ def get_bool(bytearray_: Buffer, byte_index: int, bool_index: int) -> bool:
|
|
|
32
32
|
return current_value == index_value
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def get_byte(bytearray_: Buffer, byte_index: int) ->
|
|
35
|
+
def get_byte(bytearray_: Buffer, byte_index: int) -> int:
|
|
36
36
|
"""Get byte value from bytearray.
|
|
37
37
|
|
|
38
38
|
Notes:
|
|
@@ -48,11 +48,11 @@ def get_byte(bytearray_: Buffer, byte_index: int) -> bytes:
|
|
|
48
48
|
data = bytearray_[byte_index : byte_index + 1]
|
|
49
49
|
data[0] = data[0] & 0xFF
|
|
50
50
|
packed = struct.pack("B", *data)
|
|
51
|
-
value:
|
|
51
|
+
value: int = struct.unpack("B", packed)[0]
|
|
52
52
|
return value
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def get_word(bytearray_: Buffer, byte_index: int) ->
|
|
55
|
+
def get_word(bytearray_: Buffer, byte_index: int) -> int:
|
|
56
56
|
"""Get word value from bytearray.
|
|
57
57
|
|
|
58
58
|
Notes:
|
|
@@ -73,7 +73,7 @@ def get_word(bytearray_: Buffer, byte_index: int) -> bytearray:
|
|
|
73
73
|
data[1] = data[1] & 0xFF
|
|
74
74
|
data[0] = data[0] & 0xFF
|
|
75
75
|
packed = struct.pack("2B", *data)
|
|
76
|
-
value:
|
|
76
|
+
value: int = struct.unpack(">H", packed)[0]
|
|
77
77
|
return value
|
|
78
78
|
|
|
79
79
|
|
|
@@ -122,7 +122,7 @@ def get_uint(bytearray_: Buffer, byte_index: int) -> int:
|
|
|
122
122
|
>>> get_uint(data, 0)
|
|
123
123
|
65535
|
|
124
124
|
"""
|
|
125
|
-
return
|
|
125
|
+
return get_word(bytearray_, byte_index)
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def get_real(bytearray_: Buffer, byte_index: int) -> float:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|