forked from public/pysim
456c7873eb
pySim-smpp2sim currently claims to support every feature flag that exists, but this immediately dies here with a SJA5: NotImplementedError: No handler method for ProvideLocalInformation(...) Answer anything unhandled with "performed_successfully" and log. Never "command_beyond_terminal_capability", tho, answering that for PROVIDE LOCAL INFORMATION refuses to open the session at all. Change-Id: I1e55d6f88c872a04b89a3946dd840d30b329621e
544 lines
28 KiB
Python
544 lines
28 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Bearer Independent Protocol relay"""
|
|
|
|
#
|
|
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# A ProactiveHandler with TCP sockets that backs the BIP channels,
|
|
# so a card can run its own IP session (SCP81/HTTPS, CAT_TP, ...)
|
|
#
|
|
# Currently used by pySim-smpp2sim.py which connects the SMS path to its SMPP server.
|
|
# Other drivers can pass their own sinks:
|
|
#
|
|
# handler = Proact(data_available_sink=..., sms_sink=...)
|
|
# tp = init_reader(opts, proactive_handler=handler)
|
|
|
|
|
|
import logging
|
|
import socket
|
|
import threading
|
|
import time
|
|
|
|
from osmocom.utils import b2h, h2b
|
|
|
|
from pySim.transport import ProactiveHandler
|
|
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
|
|
from pySim.cat import (ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload,
|
|
BearerDescription, DeviceIdentities, Address, OtherAddress,
|
|
UiccTransportLevel, BufferSize, ChannelStatus, ChannelData,
|
|
ChannelDataLength, EventList, EventDownload, Result)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ProactChannel:
|
|
"""One BIP channel, TS 102 223 class "e", backed by a blocking TCP socket.
|
|
|
|
Created by ProactChannels.channel_create(). A reader thread fills the Rx buffer from the
|
|
socket, the Proact handlers drain it (RECEIVE DATA) and write to it (SEND DATA). Payload
|
|
is opaque, TLS or CAT_TP run on the card.
|
|
|
|
Args:
|
|
channels: the owning ProactChannels, notified of data arrival and of close()
|
|
chan_nr: channel number 1..7 as used in the Device identities
|
|
"""
|
|
# Why blocking sockets and not Twisted endpoints, considering we have twisted?
|
|
# The proactive-command loop lives in a blocking while-loop,
|
|
# "pySim.transport.LinkBase.send_apdu_checksw" that runs on the Twisted reactor thread.
|
|
# A Twisted async TCP client only makes any progress when the reactor uhh... reacts, but
|
|
# the reactor is stuck in that loop for the whole proactive session -> the
|
|
# connectProtocol() Deferred never fires while we are handling OPEN/SEND/RECEIVE CHANNEL.
|
|
# Plain blocking sockets just work: connect() in handle_OpenChannel, send() in
|
|
# handle_SendData, recv() feeding a buffer for handle_ReceiveData. No need to make it
|
|
# harder than it has to be to handle the "massive" T0 bandwidth..
|
|
# how much we try to read off the socket per recv()
|
|
RECV_CHUNK = 4096
|
|
|
|
def __init__(self, channels: 'ProactChannels', chan_nr: int):
|
|
self.channels = channels
|
|
self.chan_nr = chan_nr
|
|
self.sock = None
|
|
# TS 102 223 says the terminal keeps an Rx buffer per channel; RECEIVE
|
|
# DATA drains it, and it is filled asynchronously as the peer sends.
|
|
self.rx_buf = bytearray()
|
|
self._rx_lock = threading.Lock()
|
|
self._reader = None
|
|
self._closing = False
|
|
self.peer_closed = False
|
|
|
|
def connect(self, host: str, port: int, timeout: float = 10.0):
|
|
"""Open the blocking TCP socket and start the background Rx reader."""
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
try:
|
|
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
s.settimeout(timeout)
|
|
s.connect((host, port))
|
|
# Back to blocking mode for the reader thread.
|
|
# CLOSE CHANNEL unblocks the pending recv() via shutdown().
|
|
s.settimeout(None)
|
|
except OSError:
|
|
s.close()
|
|
raise
|
|
self.sock = s
|
|
self._reader = threading.Thread(target=self._rx_loop,
|
|
name='bip-rx-%d' % self.chan_nr, daemon=True)
|
|
self._reader.start()
|
|
|
|
def _rx_loop(self):
|
|
"""Continuously read from the socket into rx_buf, like a real ME.
|
|
|
|
TS 102 223 7.5.10.1 says the event is raised 'only if the targeted channel buffer is
|
|
empty when new data arrives in it', so the data available hook fires on the
|
|
empty->non-empty transition only. That is enough: every RECEIVE DATA response tells
|
|
the card how many bytes remain, so it keeps fetching until the buffer is empty, and
|
|
the next event restarts it when more data arrives."""
|
|
while not self._closing:
|
|
try:
|
|
data = self.sock.recv(self.RECV_CHUNK)
|
|
except (OSError, ValueError):
|
|
break
|
|
if not data:
|
|
self.peer_closed = True
|
|
break
|
|
with self._rx_lock:
|
|
was_empty = len(self.rx_buf) == 0
|
|
self.rx_buf.extend(data)
|
|
if was_empty and not self._closing:
|
|
self.channels.notify_data_available(self)
|
|
|
|
def send(self, data: bytes):
|
|
"""Tx, write bytes to the socket == SEND DATA"""
|
|
self.sock.sendall(data)
|
|
|
|
def available_rx(self) -> int:
|
|
"""Number of bytes waiting in the Rx buffer, what RECEIVE DATA can return right now."""
|
|
with self._rx_lock:
|
|
return len(self.rx_buf)
|
|
|
|
def take_rx(self, n: int):
|
|
"""Take up to n bytes out of the Rx buffer. Returns (bytes, bytes still remaining)."""
|
|
with self._rx_lock:
|
|
chunk = bytes(self.rx_buf[:n])
|
|
del self.rx_buf[:n]
|
|
remaining = len(self.rx_buf)
|
|
return chunk, remaining
|
|
|
|
def wait_rx(self, timeout: float) -> int:
|
|
"""wait up to timeout seconds until the rxbuf has data
|
|
returns the number of bytes available
|
|
Cards have a "data available" event, card free callers use
|
|
this to wait for the echoed bytes."""
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
avail = self.available_rx()
|
|
if avail or self.peer_closed:
|
|
return avail
|
|
time.sleep(0.005)
|
|
return self.available_rx()
|
|
|
|
def close(self):
|
|
"""Close channel: stop reader, close socket, drop bookkeeping."""
|
|
self._closing = True
|
|
if self.sock is not None:
|
|
try:
|
|
self.sock.shutdown(socket.SHUT_RDWR)
|
|
except OSError:
|
|
pass
|
|
try:
|
|
self.sock.close()
|
|
except OSError:
|
|
pass
|
|
# CLOSE CHANNEL synchronously handled inside the rx reader thread
|
|
# (data-available -> ENVELOPE -> FETCH -> handle_CloseChannel -> close),
|
|
# so close() can be called on the reader thread.
|
|
# Joining self raises "cannot join current thread" so better skip i..
|
|
# setting _closing + shutting down the socket already makes _rx_loop
|
|
# return on the next iteration anyway.
|
|
if self._reader is not None and self._reader is not threading.current_thread():
|
|
self._reader.join(timeout=1.0)
|
|
self.channels.channel_delete(self.chan_nr)
|
|
|
|
class ProactChannels:
|
|
"""The open BIP channels of one terminal, keyed by channel number.
|
|
|
|
Args:
|
|
on_data_available: callback(chan: ProactChannel), invoked from the channel's reader
|
|
thread when data arrives in an empty Rx buffer. Proact turns it into an
|
|
ENVELOPE EVENT DOWNLOAD (data available).
|
|
"""
|
|
def __init__(self, on_data_available=None):
|
|
self.channels = {}
|
|
self._on_data_available = on_data_available
|
|
|
|
def channel_create(self) -> ProactChannel:
|
|
"""Create a new proactive channel, allocating its integer number."""
|
|
for i in range(1, 8):
|
|
if not i in self.channels:
|
|
self.channels[i] = ProactChannel(self, i)
|
|
return self.channels[i]
|
|
raise ValueError('Cannot allocate another channel: All channels active')
|
|
|
|
def channel_delete(self, chan_nr: int):
|
|
"""Forget a channel, called by ProactChannel.close()."""
|
|
self.channels.pop(chan_nr, None)
|
|
|
|
def notify_data_available(self, chan: ProactChannel):
|
|
"""Run the on_data_available callback for chan, if one was given."""
|
|
if self._on_data_available:
|
|
self._on_data_available(chan)
|
|
|
|
class Proact(ProactiveHandler):
|
|
"""ProactiveHandler that answers the BIP proactive commands with TCP sockets.
|
|
|
|
The transport calls the handle_* methods with the decoded proactive command and posts
|
|
the returned IE list as TERMINAL RESPONSE.
|
|
|
|
Args:
|
|
data_available_sink: callback(envelope_hex: str), called from a channel reader thread
|
|
with an encoded ENVELOPE EVENT DOWNLOAD (data available). The caller forwards it
|
|
to the card with the ENVELOPE command, the card then FETCHes RECEIVE DATA.
|
|
None: the event is only logged (card free / test mode).
|
|
sms_sink: callback(pdu), called with the SMPP deliver_sm of a SEND SHORT MESSAGE
|
|
the card issued; pySim-smpp2sim.py hands it to its SMPP server.
|
|
None: the SMS is logged and dropped.
|
|
"""
|
|
def __init__(self, data_available_sink=None, sms_sink=None):
|
|
self.data_available_sink = data_available_sink
|
|
self.sms_sink = sms_sink
|
|
self.channels = ProactChannels(on_data_available=self._on_channel_data_available)
|
|
|
|
def receive_fetch(self, pcmd: ProactiveCommand):
|
|
"""Answer anything this handler has no specific handler for.
|
|
|
|
A card coming up will usually issue PROVIDE LOCAL INFORMATION,
|
|
POLL INTERVAL or TIMER MANAGEMENT before it gets anywhere near a BIP channel
|
|
because we claim to support every feature flag there is.
|
|
|
|
Note that this is not the spec-correct answer. TS 102 223 6.8.7
|
|
says a successful TERMINAL RESPONSE to PROVIDE LOCAL INFORMATION "shall" carry the
|
|
requested Local information data object, and 6.8.13/6.8.14 says the same for TIMER
|
|
MANAGEMENT, this returns empty results for all of them, which works with real cards.
|
|
|
|
Always "performed_successfully", never "command_beyond_terminal_capability" because
|
|
answering that to PROVIDE LOCAL INFORMATION makes a card refuse to open the session.
|
|
"""
|
|
logger.info("no handler for %s, answering performed_successfully",
|
|
type(pcmd.decoded).__name__)
|
|
return self.prepare_response(pcmd, 'performed_successfully')
|
|
|
|
@staticmethod
|
|
def _find_first_element_of_type(instlist, cls):
|
|
for i in instlist:
|
|
if isinstance(i, cls):
|
|
return i
|
|
return None
|
|
|
|
@staticmethod
|
|
def _channel_nr_from_dev_ids(dev_id_ie: DeviceIdentities) -> int:
|
|
"""Maps id like channel_1 -> channel number.
|
|
TS 102 223 Section 8.7 says low nibble is channel number,
|
|
channel-N = 0x21..0x27"""
|
|
dest = dev_id_ie.decoded['dest_dev_id']
|
|
return DeviceIdentities.DEV_IDS.inverse[dest] & 0x0f
|
|
|
|
def _channel_for(self, dev_id_ie: DeviceIdentities):
|
|
"""Resolve the ProactChannel addressed by a command dev id, or None"""
|
|
return self.channels.channels.get(self._channel_nr_from_dev_ids(dev_id_ie), None)
|
|
|
|
@staticmethod
|
|
def _channel_status(chan_nr: int, established: bool = True) -> str:
|
|
"""TS 102 223 Section 8.56 channel status value for the
|
|
default/network bearer:
|
|
- byte 3 low 3 bits = channel id
|
|
- bit 8 = link established
|
|
- byte 4 = 00 no further info"""
|
|
b3 = (0x80 if established else 0x00) | (chan_nr & 0x07)
|
|
return '%02x00' % b3
|
|
|
|
def _bip_response_head(self, pcmd: ProactiveCommand,
|
|
general_result: str = 'performed_successfully',
|
|
additional_information: str = ''):
|
|
"""CommandDetails / DeviceIdentities / Result head part of a BIP TERMINAL
|
|
RESPONSE. Built on prepare_response() but with two changes:
|
|
|
|
- Device identities forced source=terminal, dest=UICC.
|
|
TS 102 223 6.8.2 mandates for every TERMINAL RESPONSE
|
|
prepare_response() inverts the commands device id, which is
|
|
right for a uicc->terminal command but would yield a wrong
|
|
channel_N->UICC for the channel addressed BIP commands.
|
|
|
|
- Result is recreated for non success cases. prepare_response()
|
|
hard codes empty "additional information", but for enum results
|
|
like BIP error -> AddlInfoBip the empty value cannot be encoded at
|
|
all, so we always ask prepare_response() for a success Result
|
|
and swap for a properly encoded one here."""
|
|
head = self.prepare_response(pcmd, 'performed_successfully')
|
|
for i, ie in enumerate(head):
|
|
if isinstance(ie, DeviceIdentities):
|
|
head[i] = DeviceIdentities(decoded={'source_dev_id': 'terminal',
|
|
'dest_dev_id': 'uicc'})
|
|
elif isinstance(ie, Result) and general_result != 'performed_successfully':
|
|
res = Result()
|
|
res.from_dict({'result': {'general_result': general_result,
|
|
'additional_information': additional_information}})
|
|
head[i] = res
|
|
return head
|
|
|
|
def _build_data_available_envelope(self, chan: ProactChannel) -> bytes:
|
|
"""TS 102 223 7.5.10.2 ENVELOPE EVENT DOWNLOAD
|
|
Event list, Device id terminal->UICC, Channel status,
|
|
Channel data length (bytes available or FF for > 255)."""
|
|
avail = min(chan.available_rx(), 0xff)
|
|
ed = EventDownload(children=[
|
|
EventList(decoded=['data_available']),
|
|
DeviceIdentities(decoded={'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}),
|
|
ChannelStatus(decoded=self._channel_status(chan.chan_nr)),
|
|
ChannelDataLength(decoded=avail),
|
|
])
|
|
return ed.to_tlv()
|
|
|
|
def _on_channel_data_available(self, chan: ProactChannel):
|
|
"""rx reader thread hook: socket data arrived while the channel buffer
|
|
was empty. card uses ENVELOPE EVENT DOWNLOAD + responds by FETCHing RECEIVE DATA
|
|
proactive command. Card free only builds and logs"""
|
|
envelope_hex = b2h(self._build_data_available_envelope(chan))
|
|
logger.info("channel %u: %u byte(s) available -> ENVELOPE(Data available) %s",
|
|
chan.chan_nr, chan.available_rx(), envelope_hex)
|
|
if self.data_available_sink:
|
|
self.data_available_sink(envelope_hex)
|
|
|
|
# handle_*: called by the transport with the decoded proactive command, the returned IE
|
|
# list becomes the TERMINAL RESPONSE.
|
|
def handle_SendShortMessage(self, pcmd: ProactiveCommand):
|
|
# {'smspp_download': [{'device_identities': {'source_dev_id': 'network',
|
|
# 'dest_dev_id': 'uicc'}},
|
|
# {'address': {'ton_npi': {'ext': True,
|
|
# 'type_of_number': 'international',
|
|
# 'numbering_plan_id': 'isdn_e164'},
|
|
# 'call_number': '79'}},
|
|
# {'sms_tpdu': {'tpdu': '40048111227ff6407070611535004d02700000481516011212000001fe4c0943aea42e45021c078ae06c66afc09303608874b72f58bacadb0dcf665c29349c799fbb522e61709c9baf1890015e8e8e196e36153106c8b92f95153774'}}
|
|
# ]}
|
|
"""SEND SHORT MESSAGE: hand the MO-SMS to sms_sink, answer with success so the card
|
|
continues with the next part of a multi part response."""
|
|
logger.info("SendShortMessage")
|
|
logger.info(pcmd)
|
|
# Relevant parts in pcmd: Address, SMS_TPDU
|
|
addr_ie = Proact._find_first_element_of_type(pcmd.children, Address)
|
|
sms_tpdu_ie = Proact._find_first_element_of_type(pcmd.children, SMS_TPDU)
|
|
raw_tpdu = sms_tpdu_ie.decoded['tpdu']
|
|
submit = SMS_SUBMIT.from_bytes(raw_tpdu)
|
|
submit.tp_da = AddressField(addr_ie.decoded['call_number'], addr_ie.decoded['ton_npi']['type_of_number'],
|
|
addr_ie.decoded['ton_npi']['numbering_plan_id'])
|
|
logger.info(submit)
|
|
self.send_sms_via_smpp(submit)
|
|
# Return a successful TERMINAL RESPONSE.
|
|
# This is important:
|
|
# - without it the transport cannot complete the proactive command
|
|
# - for a multi part OTA response, the card would never be asked to give us
|
|
# the remaining SMS chunks.
|
|
# 'pcmd' is a decoded SendShortMessage IE, which contains CommandDetails and
|
|
# DeviceIdentities that prepare_response() echoes/inverts.
|
|
return self.prepare_response(pcmd)
|
|
|
|
def handle_OpenChannel(self, pcmd: ProactiveCommand):
|
|
"""OPEN CHANNEL: connect a TCP socket to the given address and port, allocate a
|
|
channel number and report it in the Channel status of the response."""
|
|
# {'open_channel': [{'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'open_channel',
|
|
# 'command_qualifier': 3}},
|
|
# {'device_identities': {'source_dev_id': 'uicc',
|
|
# 'dest_dev_id': 'terminal'}},
|
|
# {'bearer_description': {'bearer_type': 'default',
|
|
# 'bearer_parameters': ''}},
|
|
# {'buffer_size': 1024},
|
|
# {'uicc_transport_level': {'protocol_type': 'tcp_uicc_client_remote',
|
|
# 'port_number': 32768}},
|
|
# {'other_address': {'type_of_address': 'ipv4',
|
|
# 'address': '01020304'}}
|
|
# ]}
|
|
logger.info("OpenChannel")
|
|
logger.info(pcmd)
|
|
transp_lvl_ie = Proact._find_first_element_of_type(pcmd.children, UiccTransportLevel)
|
|
other_addr_ie = Proact._find_first_element_of_type(pcmd.children, OtherAddress)
|
|
bearer_desc_ie = Proact._find_first_element_of_type(pcmd.children, BearerDescription)
|
|
buffer_size_ie = Proact._find_first_element_of_type(pcmd.children, BufferSize)
|
|
if transp_lvl_ie.decoded['protocol_type'] != 'tcp_uicc_client_remote':
|
|
raise ValueError('Unsupported protocol_type')
|
|
if other_addr_ie.decoded.get('type_of_address', None) != 'ipv4':
|
|
raise ValueError('Unsupported type_of_address')
|
|
addr_bytes = h2b(other_addr_ie.decoded['address']) if isinstance(
|
|
other_addr_ie.decoded['address'], str) else other_addr_ie.decoded['address']
|
|
ipv4_str = '%u.%u.%u.%u' % (addr_bytes[0], addr_bytes[1], addr_bytes[2], addr_bytes[3])
|
|
port_nr = transp_lvl_ie.decoded['port_number']
|
|
logger.info("OpenChannel: connecting to %s:%u", ipv4_str, port_nr)
|
|
channel = self.channels.channel_create()
|
|
# yes, blocking connect()
|
|
try:
|
|
channel.connect(ipv4_str, port_nr)
|
|
except OSError as e:
|
|
logger.warning("OpenChannel: connect to %s:%u failed: %s", ipv4_str, port_nr, e)
|
|
self.channels.channel_delete(channel.chan_nr)
|
|
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
|
'channel_closed') + [
|
|
ChannelStatus(decoded=self._channel_status(channel.chan_nr, established=False)),
|
|
bearer_desc_ie, buffer_size_ie]
|
|
|
|
# Terminal Response example: [
|
|
# {'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'open_channel',
|
|
# 'command_qualifier': 3}},
|
|
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
|
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
|
# {'channel_status': '8100'},
|
|
# {'bearer_description': {'bearer_type': 'default', 'bearer_parameters': ''}},
|
|
# {'buffer_size': 1024}
|
|
# ]
|
|
return self._bip_response_head(pcmd) + [
|
|
ChannelStatus(decoded=self._channel_status(channel.chan_nr)),
|
|
bearer_desc_ie, buffer_size_ie]
|
|
|
|
def handle_CloseChannel(self, pcmd: ProactiveCommand):
|
|
"""CLOSE CHANNEL: close the socket of the addressed channel and free its number."""
|
|
logger.info("CloseChannel")
|
|
logger.info(pcmd)
|
|
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
|
chan = self._channel_for(dev_id_ie)
|
|
if chan is None:
|
|
# channel closed / invalid
|
|
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
|
'channel_id_not_valid')
|
|
chan.close()
|
|
return self._bip_response_head(pcmd)
|
|
|
|
def handle_ReceiveData(self, pcmd: ProactiveCommand):
|
|
"""RECEIVE DATA: the card fetches up to Channel data length bytes from the Rx buffer
|
|
of the addressed channel, the response also carries how many bytes remain."""
|
|
# {'receive_data': [{'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'receive_data',
|
|
# 'command_qualifier': 0}},
|
|
# {'device_identities': {'source_dev_id': 'uicc',
|
|
# 'dest_dev_id': 'channel_1'}},
|
|
# {'channel_data_length': 9}
|
|
# ]}
|
|
logger.info("ReceiveData")
|
|
logger.info(pcmd)
|
|
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
|
req_len_ie = Proact._find_first_element_of_type(pcmd.children, ChannelDataLength)
|
|
chan = self._channel_for(dev_id_ie)
|
|
if chan is None:
|
|
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
|
'channel_id_not_valid')
|
|
# TS 102 223 8.54: RECEIVE DATA contains the requested count the card wants
|
|
requested = req_len_ie.decoded if req_len_ie is not None else chan.available_rx()
|
|
data, remaining = chan.take_rx(requested)
|
|
# TS 102 223 6.4.29:
|
|
# - return data available in the Rx buffer + num bytes still remaining (FF if > 255)
|
|
# - if fewer than requested available terminal must NOT wait, report and returns what we have
|
|
general_result = 'performed_successfully'
|
|
if len(data) < requested:
|
|
general_result = 'performed_with_missing_information'
|
|
# Terminal Response example: [
|
|
# {'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'receive_data',
|
|
# 'command_qualifier': 0}},
|
|
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
|
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
|
# {'channel_data': '16030100040e000000'},
|
|
# {'channel_data_length': 0}
|
|
# ]
|
|
return self._bip_response_head(pcmd, general_result) + [
|
|
ChannelData(decoded=b2h(data)),
|
|
ChannelDataLength(decoded=min(remaining, 0xff))]
|
|
|
|
def handle_SendData(self, pcmd: ProactiveCommand):
|
|
"""SEND DATA: write the Channel data of the command to the socket of the addressed
|
|
channel."""
|
|
# {'send_data': [{'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'send_data',
|
|
# 'command_qualifier': 1}},
|
|
# {'device_identities': {'source_dev_id': 'uicc',
|
|
# 'dest_dev_id': 'channel_1'}},
|
|
# {'channel_data': '160301003c010000380303d0f45e12b52ce5bb522750dd037738195334c87a46a847fe2b6886cada9ea6bf00000a00ae008c008b00b0002c010000050001000101'}
|
|
# ]}
|
|
logger.info("SendData")
|
|
logger.info(pcmd)
|
|
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
|
chan_data_ie = Proact._find_first_element_of_type(pcmd.children, ChannelData)
|
|
chan = self._channel_for(dev_id_ie)
|
|
if chan is None:
|
|
return self._bip_response_head(pcmd, 'bearer_independent_protocol_error',
|
|
'channel_id_not_valid')
|
|
# lets accept hexstrings as well
|
|
payload = chan_data_ie.decoded
|
|
if isinstance(payload, str):
|
|
payload = h2b(payload)
|
|
# command_qualifier bit 1 selects 'send immediately' / Tx-buffer store and forward
|
|
# For TCP stream all we have is a socket and TCP takes care of segmentation,
|
|
# so just send.
|
|
chan.send(payload)
|
|
# Terminal Response example: [
|
|
# {'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'send_data',
|
|
# 'command_qualifier': 1}},
|
|
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
|
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
|
# {'channel_data_length': 255}
|
|
# ]
|
|
# TS 102 223 6.4.30 / 8.54 Channel data length = free space tx buf; FF == > 255 available
|
|
return self._bip_response_head(pcmd) + [ChannelDataLength(decoded=255)]
|
|
|
|
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
|
|
"""SET UP EVENT LIST: acknowledged, data available and channel status are always on."""
|
|
# {'set_up_event_list': [{'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'set_up_event_list',
|
|
# 'command_qualifier': 0}},
|
|
# {'device_identities': {'source_dev_id': 'uicc',
|
|
# 'dest_dev_id': 'terminal'}},
|
|
# {'event_list': ['data_available', 'channel_status']}
|
|
# ]}
|
|
logger.info("SetUpEventList")
|
|
logger.info(pcmd)
|
|
# Terminal Response example: [
|
|
# {'command_details': {'command_number': 1,
|
|
# 'type_of_command': 'set_up_event_list',
|
|
# 'command_qualifier': 0}},
|
|
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
|
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}}
|
|
# ]
|
|
return self.prepare_response(pcmd)
|
|
|
|
def getChannelStatus(self, pcmd: ProactiveCommand):
|
|
logger.info("GetChannelStatus")
|
|
logger.info(pcmd)
|
|
return self.prepare_response(pcmd) + []
|
|
|
|
def send_sms_via_smpp(self, submit: SMS_SUBMIT):
|
|
# while in a normal network the phone/ME would *submit* a message to the SMSC,
|
|
# we are actually emulating the SMSC itself, so we must *deliver* the message
|
|
# to the ESME
|
|
deliver = SMS_DELIVER.from_submit(submit)
|
|
deliver_smpp = deliver.to_smpp()
|
|
|
|
if self.sms_sink is None:
|
|
logger.info('no sms_sink: dropping MO-SMS %s', deliver_smpp)
|
|
return
|
|
self.sms_sink(deliver_smpp)
|
|
# # obtain the connection/binding of system_id to be used for delivering MO-SMS to the ESME
|
|
# connection = smpp_server.getBoundConnections[system_id].getNextBindingForDelivery()
|
|
# connection.sendDataRequest(deliver_smpp)
|
|
|
|
|
|
|