First working version of SocketTransport

This commit is contained in:
László Károlyi 2018-04-23 17:12:13 +02:00
parent be77759e8e
commit 23db1bd509
Signed by: karolyi
GPG key ID: 2DCAF25E55735BFE
10 changed files with 168 additions and 69 deletions

View file

@ -276,6 +276,10 @@ DEBUG_PACKET_NAME = {
} }
def noop(*args, **kwargs):
pass
class Logling(object): class Logling(object):
"""A simple log interface.""" """A simple log interface."""

View file

@ -22,8 +22,10 @@ You should have received a copy of the GNU Lesser General Public License
along with pyscard; if not, write to the Free Software along with pyscard; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
""" """
import re
import sys import sys
from functools import reduce from functools import reduce
from struct import unpack
PACK = 1 PACK = 1
HEX = 2 HEX = 2
@ -32,19 +34,20 @@ COMMA = 8
def padd(bytelist, length, padding='FF'): def padd(bytelist, length, padding='FF'):
""" Padds a byte list with a constant byte value (default is x0FF) """
bytelist: the byte list to padd Padds a byte list with a constant byte value (default is x0FF)
length: the total length of the resulting byte list; bytelist: the byte list to padd
no padding if length is smaller than the byte list length length: the total length of the resulting byte list;
padding: padding value (default is 0xff) no padding if length is smaller than the byte list length
padding: padding value (default is 0xff)
returns the padded bytelist returns the padded bytelist
example: example:
padd(toBytes(\"3B 65 00 00 9C 11 01 01 03\"), 16) returns padd(toBytes(\"3B 65 00 00 9C 11 01 01 03\"), 16) returns
[0x3B, 0x65, 0, 0, 0x9C, 0x11, 1, 1, 3, 0xFF, 0xFF, 0xFF, 0xFF, [0x3B, 0x65, 0, 0, 0x9C, 0x11, 1, 1, 3, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF] 0xFF, 0xFF, 0xFF]
padd(toBytes(\"3B 65 00 00 9C 11 01 01 03\"), 8) returns padd(toBytes(\"3B 65 00 00 9C 11 01 01 03\"), 8) returns
[0x3B, 0x65, 0, 0, 0x9C, 0x11, 1, 1, 3] [0x3B, 0x65, 0, 0, 0x9C, 0x11, 1, 1, 3]
""" """
if len(bytelist) < length: if len(bytelist) < length:
@ -99,8 +102,8 @@ def toBytes(bytestring):
bytestring: a byte string of the format "3B 65 00 00 9C 11 01 01 03" or bytestring: a byte string of the format "3B 65 00 00 9C 11 01 01 03" or
"3B6500009C11010103" or "3B6500 009C1101 0103" "3B6500009C11010103" or "3B6500 009C1101 0103"
""" """
from struct import unpack if type(bytestring) in set([bytes, bytearray]):
import re return list(bytestring)
packedstring = ''.join(re.split('\W+', bytestring)) packedstring = ''.join(re.split('\W+', bytestring))
if sys.version_info[0] > 2 and isinstance(packedstring, str): if sys.version_info[0] > 2 and isinstance(packedstring, str):
packedstring = packedstring.encode() packedstring = packedstring.encode()

View file

@ -21,7 +21,7 @@ from ecrterm.transmission._transmission import Transmission
from ecrterm.transmission.signals import ACK, DLE, ETX, NAK, STX, TRANSMIT_OK from ecrterm.transmission.signals import ACK, DLE, ETX, NAK, STX, TRANSMIT_OK
from ecrterm.transmission.transport_serial import SerialTransport from ecrterm.transmission.transport_serial import SerialTransport
from ecrterm.transmission.transport_socket import SocketTransport from ecrterm.transmission.transport_socket import SocketTransport
from ecrterm.utils import is_stringlike from ecrterm.utils import detect_pt_serial, is_stringlike
class A(object): class A(object):
@ -416,9 +416,9 @@ class ECR(object):
def detect_pt(self): def detect_pt(self):
# note: this only executes utils.detect_pt with the local ecrterm. # note: this only executes utils.detect_pt with the local ecrterm.
from ecrterm.utils import detect_pt if type(self.transport) is SerialTransport:
result = detect_pt(silent=False, ecr=self, timeout=2) return detect_pt_serial(timeout=2, silent=False, ecr=self)
return result return True
def parse_str(self, s): def parse_str(self, s):
return parse_represented_data(s) return parse_represented_data(s)

View file

@ -237,7 +237,7 @@ class APDUPacket(object):
if is_stringlike(blob): if is_stringlike(blob):
# lets convert our string into a bytelist. # lets convert our string into a bytelist.
blob = toBytes(blob) blob = toBytes(blob)
if isinstance(blob, list): if type(blob) is list:
# allright. # allright.
# first we detect our packetclass # first we detect our packetclass
PacketClass = Packets.detect(blob[:2]) PacketClass = Packets.detect(blob[:2])

View file

@ -25,12 +25,12 @@ class Packet(APDUPacket):
introspection, bitmap_stati) introspection, bitmap_stati)
else: else:
introspection = '**%s' % bitmap_stati introspection = '**%s' % bitmap_stati
return "%s{%s %s} %s" % ( return '%s{%s %s} %s' % (
self.__class__.__name__, toHexString([self.cmd_class]), self.__class__.__name__, toHexString([self.cmd_class]),
toHexString([self.cmd_instr]), introspection) toHexString([self.cmd_instr]), introspection)
def _handle_unknown_response(self, response, tm): def _handle_unknown_response(self, response, tm):
print("Unknown packet response %s" % response) print('Unknown packet response %s' % response)
tm.send_received() tm.send_received()
return False return False
@ -105,15 +105,15 @@ class Registration(Packet):
# look thru all arguments: all needed fixed arguments here? # look thru all arguments: all needed fixed arguments here?
if len(self.fixed_values) < 2: if len(self.fixed_values) < 2:
raise Exception( raise Exception(
"Registration Packet needs passwort and config_byte at least") 'Registration Packet needs passwort and config_byte at least')
elif len(self.fixed_values) < 3 and len(self.bitmaps) > 0: elif len(self.fixed_values) < 3 and len(self.bitmaps) > 0:
raise Exception("Registration Packet needs CC if you add a bitmap") raise Exception('Registration Packet needs CC if you add a bitmap')
# look thru all bitmaps: all bitmaps allowed? # look thru all bitmaps: all bitmaps allowed?
return True return True
def consume_fixed(self, data, length): def consume_fixed(self, data, length):
if length < 4: if length < 4:
raise Exception("Registration needs at least 4 bytes.") raise Exception('Registration needs at least 4 bytes.')
if length >= 4: if length >= 4:
# only password and byte # only password and byte
# no cc # no cc
@ -155,8 +155,8 @@ class Registration(Packet):
ret |= 0x8 ret |= 0x8
else: else:
print( print(
"Note: intermediate status not requested, but mandatory in " 'Note: intermediate status not requested, but mandatory in '
"CardComplete Terminals") 'CardComplete Terminals')
if ecr_controls_payment: if ecr_controls_payment:
ret |= 0x10 ret |= 0x10
# 0010 0000 # 0010 0000
@ -236,7 +236,7 @@ class Initialisation(Packet):
Network-Initialization. Network-Initialization.
""" """
cmd_instr = 0x93 cmd_instr = 0x93
fixed_arguments = ['password', ] fixed_arguments = ['password']
fixed_values = {'password': '123456'} fixed_values = {'password': '123456'}
wait_for_completion = True wait_for_completion = True
@ -471,7 +471,7 @@ class IntermediateStatusInformation(Packet):
return data return data
def __repr__(self): def __repr__(self):
return "IntermediateStatus{04 FF}: %s" % ( return 'IntermediateStatus{04 FF}: %s' % (
INTERMEDIATE_STATUS_CODES.get( INTERMEDIATE_STATUS_CODES.get(
self.fixed_values.get('intermediate_status', None), self.fixed_values.get('intermediate_status', None),
'No status')) 'No status'))
@ -511,7 +511,7 @@ class PacketReceivedError(Packet):
self.cmd_instr = error_code self.cmd_instr = error_code
def __repr__(self): def __repr__(self):
return "PacketReceivedERROR{84 %s}: %s" % ( return 'PacketReceivedERROR{84 %s}: %s' % (
toHexString([self.error_code]), toHexString([self.error_code]),
ERRORCODES.get(self.error_code, 'Unknown Error'), ERRORCODES.get(self.error_code, 'Unknown Error'),
) )

View file

@ -5,7 +5,7 @@ Transmission Basics.
from ecrterm.exceptions import TransmissionException, TransportLayerException from ecrterm.exceptions import TransmissionException, TransportLayerException
from ecrterm.packets.base_packets import PacketReceived from ecrterm.packets.base_packets import PacketReceived
from ecrterm.transmission.signals import ( from ecrterm.transmission.signals import (
TIMEOUT_T4_DEFAULT, TRANSMIT_ERROR, TRANSMIT_OK, TRANSMIT_TIMEOUT) TIMEOUT_T4_DEFAULT, TRANSMIT_OK, TRANSMIT_TIMEOUT)
class Transmission(object): class Transmission(object):
@ -65,9 +65,9 @@ class Transmission(object):
self.is_master = False self.is_master = False
self.last = packet self.last = packet
try: try:
history += [(False, packet), ] history += [(False, packet)]
success, response = self.transport.send(packet) success, response = self.transport.send(packet)
history += [(True, response), ] history += [(True, response)]
# we sent the packet. # we sent the packet.
# now lets wait until we get master back. # now lets wait until we get master back.
while not self.is_master: while not self.is_master:
@ -95,6 +95,5 @@ class Transmission(object):
except Exception as e: except Exception as e:
self.is_master = True self.is_master = True
raise raise
return TRANSMIT_ERROR
self.is_master = True self.is_master = True
return TRANSMIT_OK return TRANSMIT_OK

View file

@ -8,7 +8,7 @@ The Serial Layer is a transport used for
import serial import serial
from ecrterm.common import Transport from ecrterm.common import Transport, noop
from ecrterm.conv import bs2hl, hl2bs, toBytes, toHexString from ecrterm.conv import bs2hl, hl2bs, toBytes, toHexString
from ecrterm.crc import crc_xmodem16 from ecrterm.crc import crc_xmodem16
from ecrterm.exceptions import ( from ecrterm.exceptions import (
@ -33,15 +33,11 @@ def std_serial_log(instance, data, incoming=False):
print('| error in log') print('| error in log')
def noop(*args, **kwargs):
pass
class SerialMessage(object): class SerialMessage(object):
""" """
Converts a Packet into a serial message by serializing the packet Converts a Packet into a serial message by serializing the packet
and inserting it into the final Serial Packet and inserting it into the final Serial Packet
CRC and double-DLEs included. CRC and double-DLEs included.
""" """
apdu = None apdu = None

View file

@ -1,19 +1,114 @@
from socket import create_connection from binascii import hexlify
from socket import SHUT_RDWR, create_connection
from socket import timeout as exc_timeout
from struct import unpack
from typing import Tuple
from ecrterm.common import Transport from ecrterm.common import Transport, noop
from ecrterm.conv import bs2hl
from ecrterm.packets.apdu import APDUPacket
def hexformat(data: bytes) -> str:
"""Return a prettified binary data."""
hexlified = str(hexlify(data), 'ascii')
splitted = ':'.join(
hexlified[i:i + 2] for i in range(0, len(hexlified), 2))
return repr(bytes(data)) + ' -> ' + splitted
class SocketTransport(Transport): class SocketTransport(Transport):
"""Transport for TCP/IP.""" """Transport for TCP/IP."""
insert_delays = False insert_delays = False
slog = noop
def __init__(self, uri: str): def __init__(self, uri: str, debug: bool=False):
"""Setup the IP and Port.""" """Setup the IP and Port."""
prefix, ip, port = uri.split(':') prefix, ip, port = uri.split(':')
self.port = int(port) self.port = int(port)
self.ip = ip[2:] # Trim '//' from the beginning self.ip = ip[2:] # Trim '//' from the beginning
self._debug = debug
def connect(self, timeout: int=30): def connect(self, timeout: int=30) -> bool:
"""Connect to the TCP socket.""" """
self.sock = create_connection( Connect to the TCP socket. Return `True` on successful
address=(self.ip, self.port), timeout=timeout) connection, `False` on an unsuccessful one.
"""
try:
self.sock = create_connection(
address=(self.ip, self.port), timeout=timeout)
return True
except (ConnectionError, exc_timeout) as exc:
return False
def send(self, apdu, tries: int=0, no_wait: bool=False):
"""Send data."""
to_send = bytes(apdu.to_list())
self.slog(data=bs2hl(binstring=to_send), incoming=False)
total_sent = 0
msglen = len(to_send)
while total_sent < msglen:
sent = self.sock.send(to_send[total_sent:])
if self._debug:
print('sent', sent, 'bytes of', hexformat(
data=to_send[total_sent:]))
if sent == 0:
raise RuntimeError('Socket connection broken.')
total_sent += sent
if no_wait:
return True
return self.receive()
def _receive_bytes(self, length: int) -> bytes:
"""Receive and return a fixed amount of bytes."""
recv_bytes = 0
result = b''
if self._debug:
print('waiting for', length, 'bytes')
while recv_bytes < length:
chunk = self.sock.recv(length - recv_bytes)
if self._debug:
print('received', len(chunk), 'bytes:', hexformat(data=chunk))
if chunk == b'':
raise RuntimeError('Socket connection broken.')
result += chunk
recv_bytes += len(chunk)
return result
def _receive_length(self) -> Tuple[bytes, int]:
"""
Receive the 4 bytes on the socket which indicates the message
length, and return the packed and the `int` converted length.
"""
data = self._receive_bytes(length=3)
length = data[2]
if length != 0xff:
return data, length
# Need to get 2 more bytes
length = self._receive_bytes(length=2)
data += length
return data, unpack('<H', length)[0]
def _receive(self) -> bytes:
"""
Receive the response from the terminal and return is as `bytes`.
"""
data, length = self._receive_length()
if not length: # Length is 0
return data
new_data = self._receive_bytes(length=length)
return data + new_data
def receive(
self, timeout=None, *args, **kwargs) -> Tuple[bool, APDUPacket]:
"""
Receive data, return success status and ADPUPacket instance.
"""
data = self._receive()
self.slog(data=bs2hl(binstring=data), incoming=True)
return True, APDUPacket.parse(blob=data)
def close(self):
"""Shutdown and close the connection."""
self.sock.shutdown(SHUT_RDWR)
self.sock.close()

View file

@ -18,7 +18,7 @@ def ensure_bytes(v):
return v return v
def detect_pt(device='/dev/ttyUSB0', timeout=2, silent=True, ecr=None): def detect_pt_serial(device='/dev/ttyUSB0', timeout=2, silent=True, ecr=None):
""" """
connects to given serial port and tests if a PT is present. connects to given serial port and tests if a PT is present.
if present: tries to return version number or True if present: tries to return version number or True
@ -35,7 +35,7 @@ def detect_pt(device='/dev/ttyUSB0', timeout=2, silent=True, ecr=None):
if StatusEnquiry is None: if StatusEnquiry is None:
from ecrterm.packets.base_packets import StatusEnquiry, Completion from ecrterm.packets.base_packets import StatusEnquiry, Completion
def __detect_pt(port, timeout, ecr): def __detect_pt_serial(port, timeout, ecr):
e = ecr or ECR(port) e = ecr or ECR(port)
# reconnect to have lower timeout # reconnect to have lower timeout
e.transport.close() e.transport.close()
@ -54,15 +54,15 @@ def detect_pt(device='/dev/ttyUSB0', timeout=2, silent=True, ecr=None):
e.transport.connect() e.transport.connect()
if silent: if silent:
try: try:
return __detect_pt(device, timeout, ecr) return __detect_pt_serial(device, timeout, ecr)
except Exception: except Exception:
return False return False
else: else:
return __detect_pt(device, timeout, ecr) return __detect_pt_serial(device, timeout, ecr)
if __name__ == '__main__': if __name__ == '__main__':
if detect_pt(): if detect_pt_serial():
print("PT is online at ttyUSB0") print("PT is online at ttyUSB0")
else: else:
print("PT cant be found at ttyUSB0") print("PT cant be found at ttyUSB0")

View file

@ -1,46 +1,48 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Example script to demonstrate a payment process. Example script to demonstrate a payment process.
""" """
import time from ecrterm.ecr import ECR, ecr_log
from ecrterm.packets.base_packets import Registration
from ecrterm import ecr, packets, transmission
if __name__ == '__main__': if __name__ == '__main__':
def printer(lines_of_text): def printer(lines_of_text):
for line in lines_of_text: for line in lines_of_text:
print(line) print(line)
e = ecr.ECR(device='/dev/ttyUSB0') e = ECR(device='socket://192.168.1.163:20007')
# reenable logging: # reenable logging:
e.transport.slog = ecr.ecr_log e.transport.slog = ecr_log
print(e.detect_pt()) print(e.detect_pt())
if e.detect_pt(): if e.detect_pt():
e.register(config_byte=packets.base_packets.Registration.generate_config( e.register(config_byte=Registration.generate_config(
ecr_prints_receipt=False, ecr_prints_receipt=False,
ecr_prints_admin_receipt=False, ecr_prints_admin_receipt=False,
ecr_controls_admin=True, ecr_controls_admin=True,
ecr_controls_payment=True,)) ecr_controls_payment=True))
status = e.status() status = e.status()
if status: if status:
print("Status code of PT is %s" % status) print('Status code of PT is %s' % status)
# laut doku sollte 0x9c bedeuten, ein tagesabschluss erfolgt # laut doku sollte 0x9c bedeuten, ein tagesabschluss erfolgt
# bis jetzt unklar ob er es von selbst ausführt. # bis jetzt unklar ob er es von selbst ausführt.
if status == 0x9c: if status == 0x9c:
print("End Of Day") print('End Of Day')
e.end_of_day() e.end_of_day()
# last_printout() would work too: # last_printout() would work too:
printer(e.daylog) printer(e.daylog)
else: else:
print("Unknown Status Code: %s" % status) print('Unknown Status Code: %s' % status)
# status == 0xDC for ReadCard (06 C0) -> Karte drin. 0x9c karte draussen. # status == 0xDC for ReadCard (06 C0) -> Karte drin. 0x9c karte draussen.
if e.payment(50): if e.payment(50):
printer(e.last_printout()) printer(e.last_printout())
e.wait_for_status() e.wait_for_status()
e.show_text(lines=['Auf Wiedersehen!', ' ', 'Zahlung erfolgt'], beeps=0) e.show_text(
lines=['Auf Wiedersehen!', ' ', 'Zahlung erfolgt'], beeps=0)
else: else:
e.wait_for_status() e.wait_for_status()
e.show_text(lines=['Auf Wiedersehen!', ' ', 'Vorgang abgebrochen'], beeps=1) e.show_text(
lines=['Auf Wiedersehen!', ' ', 'Vorgang abgebrochen'],
beeps=1)