mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-09-28 15:06:33 +03:00
5a54dd9eda
A large OTA response (for example GP GET STATUS app registry) is split by the card into multiple SMS, each carries a TS 23.040 9.2.3.24 'concatenated short messages' IE in its UDH. Nothing recombines them, so smpp-ota-tool currently only sees the first incomplete part. Add ConcatenatedSmsReassembler that accepts TP-User-Data, buffers parts by reference number, and returns the reassembled TP-User-Data in the canonical single-part form. Non-concatenated SMS pass through unchanged. Both the 8-bit+16-bit references are supported. A reserved value in the concatenation IE is not an error, these messages are handed back as is rather than rejected, so the caller can deal with that. Reassembly leads to a result that looks like a fat single part message the card could have produced given infinite sms sizes, so the existing decode_resp path is unaffected. Feeding those parts to the ota tool needs two more fixes because the card returns the application response as several SMS via proactive SEND SHORT MESSAGE while the ENVELOPE SMS-PP DOWNLOAD itself contains the POR without a app R-APDU. smpplib poll() drains everything, so message_received_handler runs on each: - a later status-only response must not overwrite an application response already captured, or transceive_apdu finds no last_response_data and raises - a response that cannot be decoded is be logged and skipped rather raised out of client.poll() which kills the tool. Plus tests from a sja5 session. Incomplete sets are capped (oldest evicted) so they cannot pile up. Change-Id: I8c81097e607e0d055c4f031bbcc8a74d5c24a0e7
554 lines
24 KiB
Python
554 lines
24 KiB
Python
"""Code related to SMS Encoding/Decoding"""
|
|
# simplistic SMS T-PDU code, as unfortunately nobody bothered to port the python smspdu
|
|
# module to python3, and I gave up after >= 3 hours of trying and failing to do so
|
|
|
|
# (C) 2022 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 typing
|
|
import abc
|
|
import logging
|
|
from bidict import bidict
|
|
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
|
|
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
|
from construct import Prefixed, GreedyRange
|
|
from osmocom.construct import BcdAdapter, TonNpi, Bytes, GreedyBytes
|
|
from osmocom.utils import Hexstr, h2b, b2h
|
|
|
|
from smpp.pdu import pdu_types, operations
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BytesOrHex = typing.Union[Hexstr, bytes]
|
|
|
|
class UserDataHeader:
|
|
# a single IE in the user data header
|
|
ie_c = Struct('iei'/Int8ub, 'length'/Int8ub, 'value'/Bytes(this.length))
|
|
# parser for the full UDH: Length octet followed by sequence of IEs
|
|
_construct = Struct('ies'/Prefixed(Int8ub, GreedyRange(ie_c)),
|
|
'data'/GreedyBytes)
|
|
|
|
def __init__(self, ies=[]):
|
|
self.ies = ies
|
|
|
|
def __repr__(self) -> str:
|
|
return 'UDH(%r)' % self.ies
|
|
|
|
def has_ie(self, iei:int) -> bool:
|
|
for ie in self.ies:
|
|
if ie['iei'] == iei:
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['UserDataHeader', bytes]:
|
|
if isinstance(inb, str):
|
|
inb = h2b(inb)
|
|
res = cls._construct.parse(inb)
|
|
return cls(res['ies']), res['data']
|
|
|
|
def to_bytes(self) -> bytes:
|
|
return self._construct.build({'ies':self.ies, 'data':b''})
|
|
|
|
|
|
class ConcatenatedSmsReassembler:
|
|
"""3GPP TS 23.040 section 9.2.3.24 concat multi part reassembly
|
|
|
|
A large user-data payload (e.g. a big OTA response packet) is split by the
|
|
sending entity into several SMS,
|
|
each carries a
|
|
- "concat short messages" IE in its UDH that identifies the set (ref num),
|
|
- total number of parts
|
|
- this parts seqno.
|
|
supports both:
|
|
IEI 0x00, section 9.2.3.24.1 8-bit ref form
|
|
IEI 0x08, section 9.2.3.24.8 the 16-bit ref form
|
|
|
|
Feed each received TP-User-Data (UDH + payload) to add() which
|
|
returns the reassembled TP-User-Data once all parts of the set have arrived,
|
|
or None as long as parts are still missing.
|
|
|
|
A non-concatenated SMS is returned unchanged,
|
|
just like one where the concat IE holds a reserved value:
|
|
TS 23.040 9.2.3.24.1 says
|
|
- both a total of zero
|
|
- a sequence number that is zero or greater than the total
|
|
that "the receiving entity shall ignore the whole IE",
|
|
we treat the message as a single, non-concatenated one and warn, not
|
|
as an error, so the caller does not die.
|
|
|
|
The reassembled TP-User-Data is built with a UDH that contains
|
|
the non-concat IEs seen in the parts, for example the the OTA "response packet"
|
|
indicator IE 0x71, followed by the concatenated payloads in sequence order,
|
|
so exactly the single-SMS form the sender would have produced for a payload that fits
|
|
into one SMS.
|
|
This allows convenient decoding by the normal single part path."""
|
|
|
|
# IEI: Concatenated short messages, 8-bit reference number
|
|
# (see 3GPP TS 23.040 section 9.2.3.24 and section 9.2.3.24.1)
|
|
CONCAT_8BIT = 0x00
|
|
# IEI: Concatenated short message, 16-bit reference number
|
|
# (see 3GPP TS 23.040 section 9.2.3.24 and section 9.2.3.24.8)
|
|
CONCAT_16BIT = 0x08
|
|
|
|
def __init__(self, max_sets: int = 8):
|
|
# keyed by (iei, ref, total): {'parts': {seq: payload}, 'header_ies'}, insertion ordered
|
|
self.sets = {}
|
|
self.max_sets = max_sets # incomplete sets kept, oldest is dropped beyond that
|
|
|
|
@classmethod
|
|
def _parse_concat_ie(cls, ies) -> typing.Optional[typing.Tuple[int, int, int, int]]:
|
|
"""Return (iei, ref, total, seq) of the concat IE, or None"""
|
|
for ie in ies:
|
|
if ie['iei'] == cls.CONCAT_8BIT and ie['length'] == 3:
|
|
v = ie['value']
|
|
return cls.CONCAT_8BIT, v[0], v[1], v[2]
|
|
if ie['iei'] == cls.CONCAT_16BIT and ie['length'] == 4:
|
|
v = ie['value']
|
|
return cls.CONCAT_16BIT, int.from_bytes(v[0:2], 'big'), v[2], v[3]
|
|
return None
|
|
|
|
def add(self, tpud: BytesOrHex) -> typing.Optional[bytes]:
|
|
"""Add one TP-User-Data.
|
|
Returns
|
|
- the reassembled TP-User-Data if set is complete or sms not multipart,
|
|
- else None"""
|
|
if isinstance(tpud, str):
|
|
tpud = h2b(tpud)
|
|
udh, payload = UserDataHeader.from_bytes(tpud)
|
|
concat = self._parse_concat_ie(udh.ies)
|
|
if concat is None:
|
|
return tpud
|
|
iei, ref, total, seq = concat
|
|
if total < 1 or seq < 1 or seq > total:
|
|
# TS 23.040 9.2.3.24.1 / 9.2.3.24.8, total zero or seqno zero / > total:
|
|
# Ignoring the IE means the message has no valid concat IE, which is a single part message.
|
|
# Better warn and hand it back rather than raise, so we don't kill the callers receive loop/session
|
|
logger.warning('Ignoring reserved concat IE (ref=%u total=%u seq=%u), treating the '
|
|
'message as non-concat', ref, total, seq)
|
|
return tpud
|
|
# TS 23.040 9.2.3.24.1 Total is constant in a set, refno only unique per IE form -> both set identity
|
|
# - full count = seqno 1..total is present
|
|
# - part disagreeing on the total ends up as set that cannot complete like set with missing parts
|
|
key = (iei, ref, total)
|
|
if key not in self.sets and len(self.sets) >= self.max_sets:
|
|
del self.sets[next(iter(self.sets))]
|
|
s = self.sets.setdefault(key, {'parts': {}, 'header_ies': []})
|
|
s['parts'][seq] = payload
|
|
# - remember the non concat IEs (OTA 0x71 indicator for example)
|
|
# - keep first seen occurrence of each IEI,
|
|
# so app IE present only in the first segment is preserved independent of arrival order
|
|
seen = {ie['iei'] for ie in s['header_ies']}
|
|
for ie in udh.ies:
|
|
if ie['iei'] in (self.CONCAT_8BIT, self.CONCAT_16BIT):
|
|
continue
|
|
if ie['iei'] not in seen:
|
|
s['header_ies'].append(ie)
|
|
seen.add(ie['iei'])
|
|
if len(s['parts']) < total:
|
|
return None
|
|
# all parts present -> reassemble in seq order
|
|
del self.sets[(iei, ref, total)]
|
|
body = b''.join(s['parts'][i] for i in range(1, total + 1))
|
|
header = UserDataHeader(s['header_ies']).to_bytes()
|
|
return header + body
|
|
|
|
|
|
def smpp_dcs_is_8bit(dcs: pdu_types.DataCoding) -> bool:
|
|
"""Determine if the given SMPP data coding scheme is 8-bit or not."""
|
|
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/sms.py:72: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
|
|
|
|
def ensure_smpp_is_8bit(dcs: pdu_types.DataCoding):
|
|
"""Assert if given SMPP data coding scheme is not 8-bit."""
|
|
if not smpp_dcs_is_8bit(dcs):
|
|
raise ValueError('We only support 8bit coded SMS for now')
|
|
|
|
class AddressField:
|
|
"""Representation of an address field as used in SMS T-PDU."""
|
|
_construct = Struct('addr_len'/Int8ub,
|
|
'type_of_addr'/TonNpi,
|
|
'digits'/BcdAdapter(Bytes(this.addr_len//2 + this.addr_len%2)),
|
|
'tell'/Tell)
|
|
smpp_map_npi = bidict({
|
|
'UNKNOWN': 'unknown',
|
|
'ISDN': 'isdn_e164',
|
|
'DATA': 'data_x121',
|
|
'TELEX': 'telex_f69',
|
|
'LAND_MOBILE': 'sc_specific6',
|
|
'NATIONAL': 'national',
|
|
'PRIVATE': 'private',
|
|
'ERMES': 'ermes',
|
|
})
|
|
smpp_map_ton = bidict({
|
|
'UNKNOWN': 'unknown',
|
|
'INTERNATIONAL': 'international',
|
|
'NATIONAL': 'national',
|
|
'NETWORK_SPECIFIC': 'network_specific',
|
|
'SUBSCRIBER_NUMBER': 'short_code',
|
|
'ALPHANUMERIC': 'alphanumeric',
|
|
'ABBREVIATED': 'abbreviated',
|
|
})
|
|
|
|
|
|
def __init__(self, digits, ton='unknown', npi='unknown'):
|
|
self.ton = ton
|
|
self.npi = npi
|
|
self.digits = digits
|
|
|
|
def __str__(self):
|
|
return 'AddressField(TON=%s, NPI=%s, %s)' % (self.ton, self.npi, self.digits)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, inb: BytesOrHex) -> typing.Tuple['AddressField', bytes]:
|
|
"""Construct an AddressField instance from the binary T-PDU address format."""
|
|
if isinstance(inb, str):
|
|
inb = h2b(inb)
|
|
res = cls._construct.parse(inb)
|
|
#print("size: %s" % cls._construct.sizeof())
|
|
ton = res['type_of_addr']['type_of_number']
|
|
npi = res['type_of_addr']['numbering_plan_id']
|
|
# return resulting instance + remainder bytes
|
|
return cls(res['digits'][:res['addr_len']], ton, npi), inb[res['tell']:]
|
|
|
|
@classmethod
|
|
def from_smpp(cls, addr, ton, npi) -> 'AddressField':
|
|
"""Construct an AddressField from {source,dest}_addr_{,ton,npi} attributes of smpp.pdu."""
|
|
# return the resulting instance
|
|
return cls(addr.decode('ascii'), AddressField.smpp_map_ton[ton.name], AddressField.smpp_map_npi[npi.name])
|
|
|
|
def to_smpp(self):
|
|
"""Return smpp.pdo.*.source,dest}_addr_{,ton,npi} attributes for given AddressField."""
|
|
return (self.digits, self.smpp_map_ton.inverse[self.ton], self.smpp_map_npi.inverse[self.npi])
|
|
|
|
def to_bytes(self) -> bytes:
|
|
"""Encode the AddressField into the binary representation as used in T-PDU."""
|
|
num_digits = len(self.digits)
|
|
if num_digits % 2:
|
|
self.digits += 'f'
|
|
d = {
|
|
'addr_len': num_digits,
|
|
'type_of_addr': {
|
|
'ext': True,
|
|
'type_of_number': self.ton,
|
|
'numbering_plan_id': self.npi,
|
|
},
|
|
'digits': self.digits,
|
|
}
|
|
return self._construct.build(d)
|
|
|
|
|
|
class SMS_TPDU(abc.ABC):
|
|
"""Base class for a SMS T-PDU."""
|
|
def __init__(self, **kwargs):
|
|
self.tp_mti = kwargs.get('tp_mti', None)
|
|
self.tp_rp = kwargs.get('tp_rp', False)
|
|
self.tp_udhi = kwargs.get('tp_udhi', False)
|
|
self.tp_pid = kwargs.get('tp_pid', None)
|
|
self.tp_dcs = kwargs.get('tp_dcs', None)
|
|
self.tp_udl = kwargs.get('tp_udl', None)
|
|
self.tp_ud = kwargs.get('tp_ud', None)
|
|
|
|
|
|
|
|
class SMS_DELIVER(SMS_TPDU):
|
|
"""Representation of a SMS-DELIVER T-PDU. This is the Network to MS/UE (downlink) direction."""
|
|
flags_construct = BitStruct('tp_rp'/Flag,
|
|
'tp_udhi'/Flag,
|
|
'tp_sri'/Flag,
|
|
Padding(1),
|
|
'tp_lp'/Flag,
|
|
'tp_mms'/Flag,
|
|
'tp_mti'/BitsInteger(2))
|
|
|
|
def __init__(self, **kwargs):
|
|
kwargs['tp_mti'] = 0
|
|
super().__init__(**kwargs)
|
|
self.tp_lp = kwargs.get('tp_lp', False)
|
|
self.tp_mms = kwargs.get('tp_mms', False)
|
|
self.tp_oa = kwargs.get('tp_oa', None)
|
|
self.tp_scts = kwargs.get('tp_scts', None)
|
|
self.tp_sri = kwargs.get('tp_sri', False)
|
|
|
|
def __repr__(self):
|
|
return '%s(MTI=%s, MMS=%s, LP=%s, RP=%s, UDHI=%s, SRI=%s, OA=%s, PID=%2x, DCS=%x, SCTS=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_mms, self.tp_lp, self.tp_rp, self.tp_udhi, self.tp_sri, self.tp_oa, self.tp_pid, self.tp_dcs, self.tp_scts, self.tp_udl, self.tp_ud)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, inb: BytesOrHex) -> 'SMS_DELIVER':
|
|
"""Construct a SMS_DELIVER instance from the binary encoded format as used in T-PDU."""
|
|
if isinstance(inb, str):
|
|
inb = h2b(inb)
|
|
d = SMS_DELIVER.flags_construct.parse(inb)
|
|
oa, remainder = AddressField.from_bytes(inb[1:])
|
|
d['tp_oa'] = oa
|
|
offset = 0
|
|
d['tp_pid'] = remainder[offset]
|
|
offset += 1
|
|
d['tp_dcs'] = remainder[offset]
|
|
offset += 1
|
|
# TODO: further decode
|
|
d['tp_scts'] = remainder[offset:offset+7]
|
|
offset += 7
|
|
d['tp_udl'] = remainder[offset]
|
|
offset += 1
|
|
d['tp_ud'] = remainder[offset:]
|
|
return cls(**d)
|
|
|
|
def to_bytes(self) -> bytes:
|
|
"""Encode a SMS_DELIVER instance to the binary encoded format as used in T-PDU."""
|
|
outb = bytearray()
|
|
d = {
|
|
'tp_mti': self.tp_mti, 'tp_mms': self.tp_mms, 'tp_lp': self.tp_lp,
|
|
'tp_rp': self.tp_rp, 'tp_udhi': self.tp_udhi, 'tp_sri': self.tp_sri,
|
|
}
|
|
flags = SMS_DELIVER.flags_construct.build(d)
|
|
outb.extend(flags)
|
|
outb.extend(self.tp_oa.to_bytes())
|
|
outb.append(self.tp_pid)
|
|
outb.append(self.tp_dcs)
|
|
outb.extend(self.tp_scts)
|
|
outb.append(self.tp_udl)
|
|
outb.extend(self.tp_ud)
|
|
|
|
return outb
|
|
|
|
@classmethod
|
|
def from_smpp(cls, smpp_pdu) -> 'SMS_DELIVER':
|
|
"""Construct a SMS_DELIVER instance from the deliver format used by smpp.pdu."""
|
|
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
|
return cls.from_smpp_submit(smpp_pdu)
|
|
else:
|
|
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
|
|
|
@classmethod
|
|
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_DELIVER':
|
|
"""Construct a SMS_DELIVER instance from the submit format used by smpp.pdu."""
|
|
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
|
tp_oa = AddressField.from_smpp(smpp_pdu.params['source_addr'],
|
|
smpp_pdu.params['source_addr_ton'],
|
|
smpp_pdu.params['source_addr_npi'])
|
|
tp_ud = smpp_pdu.params['short_message']
|
|
d = {
|
|
'tp_lp': False,
|
|
'tp_mms': False,
|
|
'tp_oa': tp_oa,
|
|
'tp_scts': h2b('22705200000000'), # FIXME
|
|
'tp_sri': False,
|
|
'tp_rp': False,
|
|
'tp_udhi': pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET in smpp_pdu.params['esm_class'].gsmFeatures,
|
|
'tp_pid': smpp_pdu.params['protocol_id'],
|
|
'tp_dcs': 0xF6, # we only deal with binary SMS here
|
|
'tp_udl': len(tp_ud),
|
|
'tp_ud': tp_ud,
|
|
}
|
|
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):
|
|
"""Representation of a SMS-SUBMIT T-PDU. This is the MS/UE -> network (uplink) direction."""
|
|
flags_construct = BitStruct('tp_srr'/Flag, 'tp_udhi'/Flag, 'tp_rp'/Flag,
|
|
'tp_vpf'/Enum(BitsInteger(2), none=0, relative=2, enhanced=1, absolute=3),
|
|
'tp_rd'/Flag, 'tp_mti'/BitsInteger(2))
|
|
def __init__(self, **kwargs):
|
|
kwargs['tp_mti'] = 1
|
|
super().__init__(**kwargs)
|
|
self.tp_rd = kwargs.get('tp_rd', False)
|
|
self.tp_vpf = kwargs.get('tp_vpf', 'none')
|
|
self.tp_srr = kwargs.get('tp_srr', False)
|
|
self.tp_mr = kwargs.get('tp_mr', None)
|
|
self.tp_da = kwargs.get('tp_da', None)
|
|
self.tp_vp = kwargs.get('tp_vp', None)
|
|
|
|
def __repr__(self):
|
|
return '%s(MTI=%s, RD=%s, VPF=%u, RP=%s, UDHI=%s, SRR=%s, DA=%s, PID=%2x, DCS=%x, VP=%s, UDL=%u, UD=%s)' % (self.__class__.__name__, self.tp_mti, self.tp_rd, self.tp_vpf, self.tp_rp, self.tp_udhi, self.tp_srr, self.tp_da, self.tp_pid, self.tp_dcs, self.tp_vp, self.tp_udl, self.tp_ud)
|
|
|
|
@classmethod
|
|
def from_bytes(cls, inb:BytesOrHex) -> 'SMS_SUBMIT':
|
|
"""Construct a SMS_SUBMIT instance from the binary encoded format as used in T-PDU."""
|
|
offset = 0
|
|
if isinstance(inb, str):
|
|
inb = h2b(inb)
|
|
d = SMS_SUBMIT.flags_construct.parse(inb)
|
|
offset += 1
|
|
d['tp_mr']= inb[offset]
|
|
offset += 1
|
|
da, remainder = AddressField.from_bytes(inb[2:])
|
|
d['tp_da'] = da
|
|
|
|
offset = 0
|
|
d['tp_pid'] = remainder[offset]
|
|
offset += 1
|
|
d['tp_dcs'] = remainder[offset]
|
|
offset += 1
|
|
if d['tp_vpf'] == 'none':
|
|
pass
|
|
elif d['tp_vpf'] == 'relative':
|
|
# TODO: further decode
|
|
d['tp_vp'] = remainder[offset:offset+1]
|
|
offset += 1
|
|
elif d['tp_vpf'] == 'enhanced':
|
|
# TODO: further decode
|
|
d['tp_vp'] = remainder[offset:offset+7]
|
|
offset += 7
|
|
elif d['tp_vpf'] == 'absolute':
|
|
# TODO: further decode
|
|
d['tp_vp'] = remainder[offset:offset+7]
|
|
offset += 7
|
|
else:
|
|
raise ValueError('Invalid VPF: %s' % d['tp_vpf'])
|
|
d['tp_udl'] = remainder[offset]
|
|
offset += 1
|
|
d['tp_ud'] = remainder[offset:]
|
|
return cls(**d)
|
|
|
|
def to_bytes(self) -> bytes:
|
|
"""Encode a SMS_SUBMIT instance to the binary encoded format as used in T-PDU."""
|
|
outb = bytearray()
|
|
d = {
|
|
'tp_mti': self.tp_mti, 'tp_rd': self.tp_rd, 'tp_vpf': self.tp_vpf,
|
|
'tp_rp': self.tp_rp, 'tp_udhi': self.tp_udhi, 'tp_srr': self.tp_srr,
|
|
}
|
|
flags = SMS_SUBMIT.flags_construct.build(d)
|
|
outb.extend(flags)
|
|
outb.append(self.tp_mr)
|
|
outb.extend(self.tp_da.to_bytes())
|
|
outb.append(self.tp_pid)
|
|
outb.append(self.tp_dcs)
|
|
if self.tp_vpf != 'none':
|
|
outb.extend(self.tp_vp)
|
|
outb.append(self.tp_udl)
|
|
outb.extend(self.tp_ud)
|
|
return outb
|
|
|
|
@classmethod
|
|
def from_smpp(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
|
"""Construct a SMS_SUBMIT instance from the format used by smpp.pdu."""
|
|
if smpp_pdu.id == pdu_types.CommandId.submit_sm:
|
|
return cls.from_smpp_submit(smpp_pdu)
|
|
else:
|
|
raise ValueError('Unsupported SMPP commandId %s' % smpp_pdu.id)
|
|
|
|
@classmethod
|
|
def from_smpp_submit(cls, smpp_pdu) -> 'SMS_SUBMIT':
|
|
"""Construct a SMS_SUBMIT instance from the submit format used by smpp.pdu."""
|
|
ensure_smpp_is_8bit(smpp_pdu.params['data_coding'])
|
|
tp_da = AddressField.from_smpp(smpp_pdu.params['destination_addr'],
|
|
smpp_pdu.params['dest_addr_ton'],
|
|
smpp_pdu.params['dest_addr_npi'])
|
|
tp_ud = smpp_pdu.params['short_message']
|
|
#vp_smpp = smpp_pdu.params['validity_period']
|
|
#if not vp_smpp:
|
|
# vpf = 'none'
|
|
d = {
|
|
'tp_rd': True if smpp_pdu.params['replace_if_present_flag'].name == 'REPLACE' else False,
|
|
'tp_vpf': None, # vpf,
|
|
'tp_rp': False, # related to ['registered_delivery'] ?
|
|
'tp_udhi': pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET in smpp_pdu.params['esm_class'].gsmFeatures,
|
|
'tp_srr': True if smpp_pdu.params['registered_delivery'] else False,
|
|
'tp_mr': 0, # FIXME: sm_default_msg_id ?
|
|
'tp_da': tp_da,
|
|
'tp_pid': smpp_pdu.params['protocol_id'],
|
|
'tp_dcs': 0xF6, # FIXME: we only deal with binary SMS here
|
|
'tp_vp': None, # FIXME: implement VPF conversion
|
|
'tp_udl': len(tp_ud),
|
|
'tp_ud': tp_ud,
|
|
}
|
|
return cls(**d)
|
|
|
|
def to_smpp(self) -> pdu_types.PDU:
|
|
"""Translate a SMS_SUBMIT instance to a smpp.pdu.operations.SubmitSM instance."""
|
|
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT)
|
|
reg_del = pdu_types.RegisteredDelivery(pdu_types.RegisteredDeliveryReceipt.NO_SMSC_DELIVERY_RECEIPT_REQUESTED)
|
|
if self.tp_rp:
|
|
repl_if = pdu_types.ReplaceIfPresentFlag.REPLACE
|
|
else:
|
|
repl_if = pdu_types.ReplaceIfPresentFlag.DO_NOT_REPLACE
|
|
# we only deal with binary SMS here:
|
|
if self.tp_dcs != 0xF6:
|
|
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
|
|
dc = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT, pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
|
|
(daddr, ton, npi) = self.tp_da.to_smpp()
|
|
return operations.SubmitSM(service_type='',
|
|
source_addr_ton=pdu_types.AddrTon.ALPHANUMERIC,
|
|
source_addr_npi=pdu_types.AddrNpi.UNKNOWN,
|
|
source_addr='simcard',
|
|
dest_addr_ton=ton,
|
|
dest_addr_npi=npi,
|
|
destination_addr=daddr,
|
|
esm_class=esm_class,
|
|
protocol_id=self.tp_pid,
|
|
priority_flag=pdu_types.PriorityFlag.LEVEL_0,
|
|
#schedule_delivery_time,
|
|
#validity_period,
|
|
registered_delivery=reg_del,
|
|
replace_if_present_flag=repl_if,
|
|
data_coding=dc,
|
|
#sm_default_msg_id,
|
|
short_message=self.tp_ud)
|