Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions e3dc/_e3dc_rscp_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@
# Copyright 2017 Francesco Santini <francesco.santini@gmail.com>
# Licensed under a MIT license. See LICENSE for details

import copy
import socket
import struct

from ._RSCPEncryptDecrypt import RSCPEncryptDecrypt
from ._rscpLib import RscpMessage, rscpDecode, rscpEncode, rscpFrame
from ._rscpLib import RscpMessage, endianSwapUint16, rscpDecode, rscpEncode, rscpFrame
from ._rscpTags import RscpError, RscpTag, RscpType

DEFAULT_PORT = 5033
BUFFER_SIZE = 1024 * 32

# Mirrors the frame header format used by rscpFrameDecode() in _rscpLib.py:
# magic, ctrl, sec1, sec2, ns, length (all little-endian).
_FRAME_HEADER_FMT = "<HHIIIH"
_FRAME_HEADER_SIZE = struct.calcsize(_FRAME_HEADER_FMT)
_FRAME_CRC_SIZE = struct.calcsize("<I")


class RSCPAuthenticationError(Exception):
"""Class for RSCP Authentication Error Exception."""
Expand Down Expand Up @@ -69,11 +77,49 @@ def _send(self, plainMsg: RscpMessage) -> None:
self.socket.send(encData)

def _receive(self):
data = self.socket.recv(BUFFER_SIZE)
if len(data) == 0:
raise RSCPKeyError
decData = rscpDecode(self.encdec.decrypt(data))[0]
return decData
"""Read and decrypt a complete RSCP frame.

A single ``socket.recv()`` call is not guaranteed to return an
entire frame -- TCP may split larger responses (e.g. an
extensive ``EH_REQ_GET_SAVED_ERRORS`` history) across multiple
reads. This keeps reading until the frame length declared in
the header has been fully received, instead of assuming one
read is always enough.

``RSCPEncryptDecrypt.decrypt()`` is stateful across calls, but
its default (``previouslyProcessedData=None``) bookkeeping
only accounts for how much of the *immediately preceding*
chunk was consumed. That is not accurate once more than one
chunk has a non-block-aligned leftover, which would silently
corrupt the result for three or more reads. To stay correct
regardless of how many reads are needed, each iteration
re-decrypts the whole accumulated ciphertext from scratch on a
throwaway copy of the encrypt/decrypt state, and only commits
the real, persistent ``self.encdec`` state once the full frame
is known to be available -- matching exactly what happens
today when a response arrives in a single read.
"""
ciphertext = b""
while True:
chunk = self.socket.recv(BUFFER_SIZE)
if len(chunk) == 0:
raise RSCPKeyError
ciphertext += chunk

plaintext = copy.copy(self.encdec).decrypt(ciphertext)
if len(plaintext) < _FRAME_HEADER_SIZE:
continue

_, ctrl, _, _, _, length = struct.unpack(
_FRAME_HEADER_FMT, plaintext[:_FRAME_HEADER_SIZE]
)
ctrl = endianSwapUint16(ctrl)
crc_len = _FRAME_CRC_SIZE if ctrl & 0x10 else 0
needed = _FRAME_HEADER_SIZE + length + crc_len

if len(plaintext) >= needed:
decData = rscpDecode(self.encdec.decrypt(ciphertext))[0]
return decData

def sendCommand(self, plainMsg: RscpMessage) -> None:
"""Sending RSCP command.
Expand Down
Loading