pySim-smpp2sim.py: Simulate SMSC+CN+RAN+UE for OTA testing

The pySim-smpp2sim.py program exposes two interfaces:
* SMPP server-side port, so external programs can rx/tx SMS
* APDU interface towards the SIM card

It therefore emulates the SMSC, Core Network, RAND and UE parts
that would normally be encountered in an OTA setup.

Change-Id: Ie5bae9d823bca6f6c658bd455303f63bace2258c
This commit is contained in:
Harald Welte
2022-08-06 19:24:52 +02:00
committed by laforge
parent 40e795a825
commit a2bfd397ba
7 changed files with 533 additions and 1 deletions

View File

@@ -41,6 +41,7 @@ pySim consists of several parts:
shell shell
trace trace
legacy legacy
smpp2sim
library library
osmo-smdpp osmo-smdpp
sim-rest sim-rest

57
docs/smpp2sim.rst Normal file
View File

@@ -0,0 +1,57 @@
pySim-smpp2sim
==============
This is a program to emulate the entire communication path SMSC-CN-RAN-ME
that is usually between an OTA backend and the SIM card. This allows
to play with SIM OTA technology without using a mobile network or even
a mobile phone.
An external application can act as SMPP ESME and must encode (and
encrypt/sign) the OTA SMS and submit them via SMPP to this program, just
like it would submit it normally to a SMSC (SMS Service Centre). The
program then re-formats the SMPP-SUBMIT into a SMS DELIVER TPDU and
passes it via an ENVELOPE APDU to the SIM card that is locally inserted
into a smart card reader.
The path from SIM to external OTA application works the opposite way.
The default SMPP system_id is `test`. Likewise, the default SMPP
password is `test`
Running pySim-smpp2sim
----------------------
The command accepts the same command line arguments for smart card interface device selection as pySim-shell,
as well as a few SMPP specific arguments:
.. argparse::
:module: pySim-smpp2sim
:func: option_parser
:prog: pySim-smpp2sim.py
Example execution with sample output
------------------------------------
So for a simple system with a single PC/SC device, you would typically use something like
`./pySim-smpp2sim.py -p0` to start the program. You will see output like this at start-up
::
Using reader PCSC[HID Global OMNIKEY 3x21 Smart Card Reader [OMNIKEY 3x21 Smart Card Reader] 00 00]
INFO root: Binding Virtual SMSC to TCP Port 2775 at ::
The application has hence bound to local TCP port 2775 and expects your SMS-sending applications to send their
SMS there. Once you do, you will see log output like below:
::
WARNING smpp.twisted.protocol: SMPP connection established from ::ffff:127.0.0.1 to port 2775
INFO smpp.twisted.server: Added CommandId.bind_transceiver bind for 'test'. Active binds: CommandId.bind_transceiver: 1, CommandId.bind_transmitter: 0, CommandId.bind_receiver: 0. Max binds: 2
INFO smpp.twisted.protocol: Bind request succeeded for test. 1 active binds
And once your external program is sending SMS to the simulated SMSC, it will log something like
::
INFO root: SMS_DELIVER(MTI=0, MMS=False, LP=False, RP=False, UDHI=True, SRI=False, OA=AddressField(TON=international, NPI=unknown, 12), PID=7f, DCS=f6, SCTS=bytearray(b'"pR\x00\x00\x00\x00'), UDL=45, UD=b"\x02p\x00\x00(\x15\x16\x19\x12\x12\xb0\x00\x01'\xfa(\xa5\xba\xc6\x9d<^\x9d\xf2\xc7\x15]\xfd\xdeD\x9c\x82k#b\x15Ve0x{0\xe8\xbe]")
SMSPPDownload(DeviceIdentities({'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),Address({'ton_npi': 0, 'call_number': '0123456'}),SMS_TPDU({'tpdu': '400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c

428
pySim-smpp2sim.py Executable file
View File

@@ -0,0 +1,428 @@
#!/usr/bin/env python3
#
# Program to emulate the entire communication path SMSC-MSC-BSC-BTS-ME
# that is usually between an OTA backend and the SIM card. This allows
# to play with SIM OTA technology without using a mobile network or even
# a mobile phone.
#
# An external application must encode (and encrypt/sign) the OTA SMS
# and submit them via SMPP to this program, just like it would submit
# it normally to a SMSC (SMS Service Centre). The program then re-formats
# the SMPP-SUBMIT into a SMS DELIVER TPDU and passes it via an ENVELOPE
# APDU to the SIM card that is locally inserted into a smart card reader.
#
# The path from SIM to external OTA application works the opposite way.
# (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/>.
import argparse
import logging
import colorlog
from twisted.protocols import basic
from twisted.internet import defer, endpoints, protocol, reactor, task
from twisted.cred.portal import IRealm
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
from twisted.cred.portal import Portal
from zope.interface import implementer
from smpp.twisted.config import SMPPServerConfig
from smpp.twisted.server import SMPPServerFactory, SMPPBindManager
from smpp.twisted.protocol import SMPPSessionStates, DataHandlerResponse
from smpp.pdu import pdu_types, operations, pdu_encoding
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer
from pySim.commands import SimCardCommands
from pySim.cards import UiccCardBase
from pySim.exceptions import *
from pySim.cat import ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload, BearerDescription
from pySim.cat import DeviceIdentities, Address, OtherAddress, UiccTransportLevel, BufferSize
from pySim.cat import ChannelStatus, ChannelData, ChannelDataLength
from pySim.utils import b2h, h2b
logger = logging.getLogger(__name__)
# MSISDNs to use when generating proactive SMS messages
SIM_MSISDN='23'
ESME_MSISDN='12'
# HACK: we need some kind of mapping table between system_id and card-reader
# or actually route based on MSISDNs
hackish_global_smpp = None
class MyApduTracer(ApduTracer):
def trace_response(self, cmd, sw, resp):
print("-> %s %s" % (cmd[:10], cmd[10:]))
print("<- %s: %s" % (sw, resp))
class TcpProtocol(protocol.Protocol):
def dataReceived(self, data):
pass
def connectionLost(self, reason):
pass
def tcp_connected_callback(p: protocol.Protocol):
"""called by twisted TCP client."""
logger.error("%s: connected!" % p)
class ProactChannel:
"""Representation of a single proective channel."""
def __init__(self, channels: 'ProactChannels', chan_nr: int):
self.channels = channels
self.chan_nr = chan_nr
self.ep = None
def close(self):
"""Close the channel."""
if self.ep:
self.ep.disconnect()
self.channels.channel_delete(self.chan_nr)
class ProactChannels:
"""Wrapper class for maintaining state of proactive channels."""
def __init__(self):
self.channels = {}
def channel_create(self) -> ProactChannel:
"""Create a new proactive channel, allocating its integer number."""
for i in range(1, 9):
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):
del self.channels[chan_nr]
class Proact(ProactiveHandler):
#def __init__(self, smpp_factory):
# self.smpp_factory = smpp_factory
def __init__(self):
self.channels = ProactChannels()
@staticmethod
def _find_first_element_of_type(instlist, cls):
for i in instlist:
if isinstance(i, cls):
return i
return None
"""Call-back which the pySim transport core calls whenever it receives a
proactive command from the SIM."""
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'}}
# ]}
"""Card requests sending a SMS. We need to pass it on to the ESME via SMPP."""
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)
def handle_OpenChannel(self, pcmd: ProactiveCommand):
"""Card requests opening a new channel via a UDP/TCP socket."""
# {'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')
ipv4_bytes = h2b(other_addr_ie.decoded['address'])
ipv4_str = '%u.%u.%u.%u' % (ipv4_bytes[0], ipv4_bytes[1], ipv4_bytes[2], ipv4_bytes[3])
port_nr = transp_lvl_ie.decoded['port_number']
print("%s:%u" % (ipv4_str, port_nr))
channel = self.channels.channel_create()
channel.ep = endpoints.TCP4ClientEndpoint(reactor, ipv4_str, port_nr)
channel.prot = TcpProtocol()
d = endpoints.connectProtocol(channel.ep, channel.prot)
# FIXME: why is this never called despite the client showing the inbound connection?
d.addCallback(tcp_connected_callback)
# 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.prepare_response(pcmd) + [ChannelStatus(decoded='8100'), bearer_desc_ie, buffer_size_ie]
def handle_CloseChannel(self, pcmd: ProactiveCommand):
"""Close a channel."""
logger.info("CloseChannel")
logger.info(pcmd)
def handle_ReceiveData(self, pcmd: ProactiveCommand):
"""Receive/read data from the socket."""
# {'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)
# 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.prepare_response(pcmd) + []
def handle_SendData(self, pcmd: ProactiveCommand):
"""Send/write data received from the SIM to the socket."""
# {'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_str = dev_id_ie.decoded['dest_dev_id']
chan_nr = 1 # FIXME
chan = self.channels.channels.get(chan_nr, None)
# FIXME chan.prot.transport.write(h2b(chan_data_ie.decoded))
# 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}
# ]
return self.prepare_response(pcmd) + [ChannelDataLength(decoded=255)]
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
# {'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()
hackish_global_smpp.sendDataRequest(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)
def dcs_is_8bit(dcs):
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
return True
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED_COMMON):
return True
# pySim-smpp2sim.py:150:21: E1101: Instance of 'DataCodingScheme' has no 'GSM_MESSAGE_CLASS' member (no-member)
# pylint: disable=no-member
if dcs.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS and dcs.schemeData['msgCoding'] == pdu_types.DataCodingGsmMsgCoding.DATA_8BIT:
return True
else:
return False
class MyServer:
@implementer(IRealm)
class SmppRealm:
def requestAvatar(self, avatarId, mind, *interfaces):
return ('SMPP', avatarId, lambda: None)
def __init__(self, tcp_port:int = 2775, bind_ip = '::', system_id:str = 'test', password:str = 'test'):
smpp_config = SMPPServerConfig(msgHandler=self._msgHandler,
systems={system_id: {'max_bindings': 2}})
portal = Portal(self.SmppRealm())
credential_checker = InMemoryUsernamePasswordDatabaseDontUse()
credential_checker.addUser(system_id, password)
portal.registerChecker(credential_checker)
self.factory = SMPPServerFactory(smpp_config, auth_portal=portal)
logger.info('Binding Virtual SMSC to TCP Port %u at %s' % (tcp_port, bind_ip))
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
smppEndpoint.listen(self.factory)
self.tp = self.scc = self.card = None
def connect_to_card(self, tp: LinkBase):
self.tp = tp
self.scc = SimCardCommands(self.tp)
self.card = UiccCardBase(self.scc)
# this should be part of UiccCardBase, but FairewavesSIM breaks with that :/
self.scc.cla_byte = "00"
self.scc.sel_ctrl = "0004"
self.card.read_aids()
self.card.select_adf_by_aid(adf='usim')
# FIXME: create a more realistic profile than ffffff
self.scc.terminal_profile('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
def _msgHandler(self, system_id, smpp, pdu):
"""Handler for incoming messages received via SMPP from ESME."""
# HACK: we need some kind of mapping table between system_id and card-reader
# or actually route based on MSISDNs
global hackish_global_smpp
hackish_global_smpp = smpp
if pdu.id == pdu_types.CommandId.submit_sm:
return self.handle_submit_sm(system_id, smpp, pdu)
else:
logger.warning('Rejecting non-SUBMIT commandID')
return pdu_types.CommandStatus.ESME_RINVCMDID
def handle_submit_sm(self, system_id, smpp, pdu):
"""SUBMIT-SM was received via SMPP from ESME. We need to deliver it to the SIM."""
# check for valid data coding scheme + PID
if not dcs_is_8bit(pdu.params['data_coding']):
logger.warning('Rejecting non-8bit DCS')
return pdu_types.CommandStatus.ESME_RINVDCS
if pdu.params['protocol_id'] != 0x7f:
logger.warning('Rejecting non-SIM PID')
return pdu_types.CommandStatus.ESME_RINVDCS
# 1) build a SMS-DELIVER (!) from the SMPP-SUBMIT
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
logger.info(tpdu)
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
addr_ie = Address(decoded={'ton_npi': {'ext':False, 'type_of_number':'unknown', 'numbering_plan_id':'unknown'}, 'call_number': '0123456'})
dev_ids = DeviceIdentities(decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'})
sms_dl = SMSPPDownload(children=[dev_ids, addr_ie, tpdu_ie])
# 3) send to the card
envelope_hex = b2h(sms_dl.to_tlv())
logger.info("ENVELOPE: %s" % envelope_hex)
(data, sw) = self.scc.envelope(envelope_hex)
logger.info("SW %s: %s" % (sw, data))
if sw in ['9200', '9300']:
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
elif sw == '9000' or sw[0:2] in ['6f', '62', '63'] and len(data):
# data something like 027100000e0ab000110000000000000001612f or
# 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
# which is the user-data portion of the SMS starting with the UDH (027100)
# TODO: return the response back to the sender in an RP-ACK; PID/DCS like in CMD
deliver = operations.DeliverSM(service_type=pdu.params['service_type'],
source_addr_ton=pdu.params['dest_addr_ton'],
source_addr_npi=pdu.params['dest_addr_npi'],
source_addr=pdu.params['destination_addr'],
dest_addr_ton=pdu.params['source_addr_ton'],
dest_addr_npi=pdu.params['source_addr_npi'],
destination_addr=pdu.params['source_addr'],
esm_class=pdu.params['esm_class'],
protocol_id=pdu.params['protocol_id'],
priority_flag=pdu.params['priority_flag'],
data_coding=pdu.params['data_coding'],
short_message=h2b(data))
smpp.sendDataRequest(deliver)
return pdu_types.CommandStatus.ESME_ROK
else:
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
option_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
argparse_add_reader_args(option_parser)
smpp_group = option_parser.add_argument_group('SMPP Options')
smpp_group.add_argument('--smpp-bind-port', type=int, default=2775,
help='TCP Port to bind the SMPP socket to')
smpp_group.add_argument('--smpp-bind-ip', default='::',
help='IPv4/IPv6 address to bind the SMPP socket to')
smpp_group.add_argument('--smpp-system-id', default='test',
help='SMPP System-ID used by ESME to bind')
smpp_group.add_argument('--smpp-password', default='test',
help='SMPP Password used by ESME to bind')
if __name__ == '__main__':
log_format='%(log_color)s%(levelname)-8s%(reset)s %(name)s: %(message)s'
colorlog.basicConfig(level=logging.INFO, format = log_format)
logger = colorlog.getLogger()
opts = option_parser.parse_args()
tp = init_reader(opts, proactive_handler = Proact())
if tp is None:
exit(1)
tp.connect()
global g_ms
g_ms = MyServer(opts.smpp_bind_port, opts.smpp_bind_ip, opts.smpp_system_id, opts.smpp_password)
g_ms.connect_to_card(tp)
reactor.run()

View File

@@ -32,7 +32,7 @@ from pySim.utils import dec_xplmn_w_act
# TS 102 223 Section 8.1 # TS 102 223 Section 8.1
class Address(COMPR_TLV_IE, tag=0x86): class Address(COMPR_TLV_IE, tag=0x86):
_construct = Struct('ton_npi'/Int8ub, _construct = Struct('ton_npi'/TonNpi,
'call_number'/BcdAdapter(GreedyBytes)) 'call_number'/BcdAdapter(GreedyBytes))
# TS 102 223 Section 8.2 # TS 102 223 Section 8.2

View File

@@ -253,6 +253,49 @@ class SMS_DELIVER(SMS_TPDU):
} }
return cls(**d) return cls(**d)
@classmethod
def from_submit(cls, submit: 'SMS_SUBMIT') -> 'SMS_DELIVER':
"""Construct a SMS_DELIVER instance from a SMS_SUBMIT instance."""
d = {
# common fields (SMS_TPDU base class) which exist in submit, so we can copy them
'tp_mti': submit.tp_mti,
'tp_rp': submit.tp_rp,
'tp_udhi': submit.tp_udhi,
'tp_pid': submit.tp_pid,
'tp_dcs': submit.tp_dcs,
'tp_udl': submit.tp_udl,
'tp_ud': submit.tp_ud,
# SMS_DELIVER specific fields
'tp_lp': False,
'tp_mms': False,
'tp_oa': None,
'tp_scts': h2b('22705200000000'), # FIXME
'tp_sri': False,
}
return cls(**d)
def to_smpp(self) -> pdu_types.PDU:
"""Translate a SMS_DELIVER instance to a smpp.pdu.operations.DeliverSM instance."""
# we only deal with binary SMS here:
if self.tp_dcs != 0xF6:
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
dcs = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT,
gsmFeatures=[pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET])
if self.tp_oa:
oa_digits, oa_ton, oa_npi = self.tp_oa.to_smpp()
else:
oa_digits, oa_ton, oa_npi = None, None, None
return operations.DeliverSM(source_addr=oa_digits,
source_addr_ton=oa_ton,
source_addr_npi=oa_npi,
#destination_addr=ESME_MSISDN,
esm_class=esm_class,
protocol_id=self.tp_pid,
data_coding=dcs,
short_message=self.tp_ud)
class SMS_SUBMIT(SMS_TPDU): class SMS_SUBMIT(SMS_TPDU):

View File

@@ -14,3 +14,4 @@ cryptography
git+https://github.com/osmocom/asn1tools git+https://github.com/osmocom/asn1tools
packaging packaging
git+https://github.com/hologram-io/smpp.pdu git+https://github.com/hologram-io/smpp.pdu
smpp.twisted3 @ git+https://github.com/jookies/smpp.twisted

View File

@@ -33,12 +33,14 @@ setup(
"packaging", "packaging",
"smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu", "smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu",
"asn1tools", "asn1tools",
"smpp.twisted3 @ git+https://github.com/jookies/smpp.twisted",
], ],
scripts=[ scripts=[
'pySim-prog.py', 'pySim-prog.py',
'pySim-read.py', 'pySim-read.py',
'pySim-shell.py', 'pySim-shell.py',
'pySim-trace.py', 'pySim-trace.py',
'pySim-smpp2sim.py',
], ],
package_data={ package_data={
'pySim.esim': 'pySim.esim':