diff --git a/pySim-smpp2sim.py b/pySim-smpp2sim.py index 95b9ccd5..c1b2bb77 100755 --- a/pySim-smpp2sim.py +++ b/pySim-smpp2sim.py @@ -50,7 +50,7 @@ from smpp.pdu import pdu_types, operations, pdu_encoding from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField -from pySim.bip import Proact +from pySim.bip import Proact, terminal_profile from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer from pySim.commands import SimCardCommands from pySim.cards import UiccCardBase @@ -126,8 +126,7 @@ class MyServer: 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') + self.scc.terminal_profile(b2h(terminal_profile())) # Connect the BIP relay inbound path to the card. # relay socket receives data -> ME initiated ENVELOPE EVENT DOWNLOA # -> triggers RECEIVE DATA proactive session. diff --git a/pySim/bip.py b/pySim/bip.py index b858027c..549655a8 100644 --- a/pySim/bip.py +++ b/pySim/bip.py @@ -43,6 +43,35 @@ from pySim.cat import (ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownlo logger = logging.getLogger(__name__) + +def terminal_profile(num_channels: int = 7) -> bytes: + """TERMINAL PROFILE for what we implement, TS 102 223 5.2 and annex T. + + Annex T table T.1 lists what a Connected Entity, a CAT client that is not the modem + which is pretty much what we are, may announce, and its inverse is what only a modem may announce. + """ + if not 0 <= num_channels <= ProactChannels.MAX_CHANNELS: + raise ValueError('num_channels must be 0..%u' % ProactChannels.MAX_CHANNELS) + profile = bytearray(32) + # 1 (Download): b1 profile download, b2+b5 SMS-PP data download. Both of the latter, per the + # note in TS 31.111 5.2: "several bits may need to be set to 1 for the support of the same + # facility ... because of backward compatibility with SAT". The relay is OTA over SMS-PP. + profile[0] = 0x01 | 0x02 | 0x10 + profile[1] = 0x01 # 2 (Other): b1 command result + profile[2] = 0x80 # 3: b8 REFRESH (empty result is a valid answer, 6.4.7) + profile[3] = 0x02 # 4: b2 SEND SHORT MESSAGE (the OTA response path) + profile[4] = 0x01 # 5: b1 SET UP EVENT LIST + profile[5] = 0x04 | 0x08 # 6: b3 Event Data available, b4 Event Channel status + # 12 (class "e"): b1..b5 OPEN CHANNEL, CLOSE CHANNEL, RECEIVE DATA, SEND DATA, GET CHANNEL + # STATUS. + profile[11] = 0x1f + # 13 (class "e" supported bearers): b2 GPRS, and b6..b8 the number of channels. + profile[12] = 0x02 | (num_channels << 5) + profile[13] = 0x40 | 0x20 # 14: b6 no display capability, b7 no keypad available + profile[16] = 0x01 # 15: b1 TCP, UICC in client mode, remote connection + return bytes(profile) + + class ProactChannel: """One BIP channel, TS 102 223 class "e", backed by a blocking TCP socket. @@ -178,13 +207,18 @@ class ProactChannels: thread when data arrives in an empty Rx buffer. Proact turns it into an ENVELOPE EVENT DOWNLOAD (data available). """ + + # TS 102 223 8.56 channel identifier in 3 bits as "1 to 7", 0 == no channel available + # TERMINAL PROFILE has to agree with byte 13 , "number of channels supported by terminal" + MAX_CHANNELS = 7 + 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): + for i in range(1, self.MAX_CHANNELS + 1): if not i in self.channels: self.channels[i] = ProactChannel(self, i) return self.channels[i] @@ -223,8 +257,8 @@ class Proact(ProactiveHandler): """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. + POLL INTERVAL or TIMER MANAGEMENT before it gets anywhere near a BIP channel, + whatever the TERMINAL PROFILE announces. 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 diff --git a/tests/unittests/test_bip_relay.py b/tests/unittests/test_bip_relay.py index f5155bcb..46281bd0 100644 --- a/tests/unittests/test_bip_relay.py +++ b/tests/unittests/test_bip_relay.py @@ -30,7 +30,7 @@ from pySim.cat import (ProactiveCommand, CommandDetails, DeviceIdentities, OtherAddress, ChannelData, ChannelDataLength, ChannelStatus, Result) -from pySim.bip import Proact +from pySim.bip import Proact, ProactChannels, terminal_profile class _EchoServer: @@ -329,6 +329,44 @@ class OpenChannelRefusalTest(unittest.TestCase): self.assertEqual(self.proact.channels.channels, {}) # channel given back +class TerminalProfileTest(unittest.TestCase): + """TS 102 223 5.2, one bit per CAT facility""" + + def setUp(self): + self.profile = terminal_profile() + + def byte(self, n): + return self.profile[n - 1] # 1-based, as 5.2 numbers them + + def test_announced(self): + self.assertEqual(len(self.profile), 32) + self.assertEqual(self.byte(1), 0x13) # profile download, SMS-PP download b2+b5 + self.assertEqual(self.byte(4), 0x02) # SEND SHORT MESSAGE + self.assertEqual(self.byte(5) & 0x01, 0x01) # SET UP EVENT LIST + self.assertEqual(self.byte(6), 0x0c) # events: data available, channel status + self.assertEqual(self.byte(12), 0x1f) # OPEN/CLOSE CHANNEL, RECEIVE/SEND DATA, STATUS + self.assertEqual(self.byte(13) >> 5, ProactChannels.MAX_CHANNELS) + self.assertEqual(self.byte(14), 0x60) # class ND, class NK + self.assertEqual(self.byte(17), 0x01) # TCP, UICC client mode, remote + + def test_not_announced(self): + self.assertEqual(self.byte(3) & 0x60, 0) # POLL INTERVAL, POLLING OFF + self.assertEqual(self.byte(4) & 0xc0, 0) # PROVIDE LOCAL INFORMATION, NMR + self.assertEqual(self.byte(12) & 0xe0, 0) # SERVICE SEARCH/INFORMATION, DECLARE SERVICE + self.assertEqual(self.byte(14) & 0x1f, 0) # no characters down the display + for n in (7, 9, 10, 11, 15, 16, 18): # class "a", class "d", display, ESN/IMEISV + self.assertEqual(self.byte(n), 0) + + def test_channel_count(self): + self.assertEqual(terminal_profile(3)[12] >> 5, 3) + with self.assertRaises(ValueError): # 8.56: 1 to 7 + terminal_profile(8) + + +if __name__ == "__main__": + unittest.main() + + class BipSinkTest(unittest.TestCase): """Both sinks are optional, a driver with no SMS path at all must not crash and burn with a card that sends one, and one that has one must get the PDU."""