3 Commits

Author SHA1 Message Date
Philipp Maier
0634f77308 esim/http_json_api: allow URL rewriting
The URL used when HTTP requests are performed is defined statically
with the url_prefix passed to the constructor of JsonHttpApiClient
together with the path property in JsonHttpApiFunction.

For applications that require dynamic URLs there is no way to rewrite
the URL. Let's add a mechanism that allows API users to apply custom
URL reqriting rules by adding a rewrite_url method to
JsonHttpApiFunction. API users may then overload this method with a
custom implementation as needed.

Related: SYS#7918
Change-Id: Id2713a867079cc140517fe312189e5e2162608a5
2026-03-17 11:17:12 +01:00
Vadim Yanitskiy
a5a5865c7c cdma_ruim: fix copy-pasted desc for EF.AD
Change-Id: I2338f35c21978dd6b8916c0abd57b94f5e087655
2026-03-10 17:43:10 +00:00
Harald Welte
3752aeb94e pySim.esim.saip.File: Support pinStatusTemplateDO + lcsi
The tuples defining a DF or ADF in an eSIM template must contain a
pinStatusTemplateDO.  When parsing tuples into a File() instance, we
must save it, and re-create it at the time we re-encode that file.

Same applies to the lcsi (life cycle state indicator), which may
optionally exist for any file.

Change-Id: I073aa4374f2cd664d07fa0224bf0d4c809cdf4aa
Closes: OS#6955
2026-03-10 16:34:48 +00:00
16 changed files with 76 additions and 88 deletions

View File

@@ -285,7 +285,10 @@ if __name__ == '__main__':
option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False) option_parser.add_argument("--admin", action='store_true', help="perform action as admin", default=False)
opts = option_parser.parse_args() opts = option_parser.parse_args()
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose) PySimLogger.setup(print, {logging.WARN: "\033[33m"})
if (opts.verbose):
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
# Open CSV file # Open CSV file
cr = open_csv(opts) cr = open_csv(opts)

View File

@@ -44,11 +44,6 @@ from pySim.legacy.ts_51_011 import EF
from pySim.card_handler import * from pySim.card_handler import *
from pySim.utils import * from pySim.utils import *
from pathlib import Path
import logging
from pySim.log import PySimLogger
log = PySimLogger.get(Path(__file__).stem)
def parse_options(): def parse_options():
@@ -190,7 +185,6 @@ def parse_options():
default=False, action="store_true") default=False, action="store_true")
parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE", parser.add_argument("--card_handler", dest="card_handler_config", metavar="FILE",
help="Use automatic card handling machine") help="Use automatic card handling machine")
parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
options = parser.parse_args() options = parser.parse_args()
@@ -776,9 +770,6 @@ if __name__ == '__main__':
# Parse options # Parse options
opts = parse_options() opts = parse_options()
# Setup logger
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Init card reader driver # Init card reader driver
sl = init_reader(opts) sl = init_reader(opts)

View File

@@ -46,17 +46,11 @@ from pySim.utils import dec_imsi, dec_iccid
from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn from pySim.legacy.utils import format_xplmn_w_act, dec_st, dec_msisdn
from pySim.ts_51_011 import EF_SMSP from pySim.ts_51_011 import EF_SMSP
from pathlib import Path
import logging
from pySim.log import PySimLogger
log = PySimLogger.get(Path(__file__).stem)
option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card', option_parser = argparse.ArgumentParser(description='Legacy tool for reading some parts of a SIM card',
formatter_class=argparse.ArgumentDefaultsHelpFormatter) formatter_class=argparse.ArgumentDefaultsHelpFormatter)
option_parser.add_argument("--verbose", help="Enable verbose logging", action='store_true', default=False)
argparse_add_reader_args(option_parser) argparse_add_reader_args(option_parser)
def select_app(adf: str, card: SimCard): def select_app(adf: str, card: SimCard):
"""Select application by its AID""" """Select application by its AID"""
sw = 0 sw = 0
@@ -81,9 +75,6 @@ if __name__ == '__main__':
# Parse options # Parse options
opts = option_parser.parse_args() opts = option_parser.parse_args()
# Setup logger
PySimLogger.setup(print, {logging.WARN: "\033[33m"}, opts.verbose)
# Init card reader driver # Init card reader driver
sl = init_reader(opts) sl = init_reader(opts)

View File

@@ -107,12 +107,12 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
kwargs = {'include_ipy': True} kwargs = {'include_ipy': True}
self.verbose = verbose self.verbose = verbose
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW}) self._onchange_verbose('verbose', False, self.verbose);
self._onchange_verbose('verbose', False, self.verbose)
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False, super().__init__(persistent_history_file='~/.pysim_shell_history', allow_cli_args=False,
auto_load_commands=False, startup_script=script, **kwargs) auto_load_commands=False, startup_script=script, **kwargs)
PySimLogger.setup(self.poutput, {logging.WARN: YELLOW})
self.intro = style(self.BANNER, fg=RED) self.intro = style(self.BANNER, fg=RED)
self.default_category = 'pySim-shell built-in commands' self.default_category = 'pySim-shell built-in commands'
self.card = None self.card = None
@@ -1175,7 +1175,13 @@ if __name__ == '__main__':
opts = option_parser.parse_args() opts = option_parser.parse_args()
# Ensure that we are able to print formatted warnings from the beginning. # Ensure that we are able to print formatted warnings from the beginning.
PySimLogger.setup(print, {logging.WARN: YELLOW}, opts.verbose) PySimLogger.setup(print, {logging.WARN: YELLOW})
if opts.verbose:
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
else:
PySimLogger.set_verbose(False)
PySimLogger.set_level(logging.INFO)
# Register csv-file as card data provider, either from specified CSV # Register csv-file as card data provider, either from specified CSV
# or from CSV file in home directory # or from CSV file in home directory

View File

@@ -128,7 +128,7 @@ class EF_AD(TransparentEF):
cell_test = 0x04 cell_test = 0x04
def __init__(self, fid='6f43', sfid=None, name='EF.AD', def __init__(self, fid='6f43', sfid=None, name='EF.AD',
desc='Service Provider Name', size=(3, None), **kwargs): desc='Administrative Data', size=(3, None), **kwargs):
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs) super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
self._construct = Struct( self._construct = Struct(
# Byte 1: Display Condition # Byte 1: Display Condition

View File

@@ -19,7 +19,7 @@ import abc
import requests import requests
import logging import logging
import json import json
from typing import Optional from typing import Optional, Tuple
import base64 import base64
from twisted.web.server import Request from twisted.web.server import Request
@@ -180,7 +180,7 @@ class JsonHttpApiFunction(abc.ABC):
# receives from the a requesting client. The same applies vice versa to class variables that have an "output_" # receives from the a requesting client. The same applies vice versa to class variables that have an "output_"
# prefix. # prefix.
# path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder') # path of the API function (e.g. '/gsma/rsp2/es2plus/confirmOrder', see also method rewrite_url).
path = None path = None
# dictionary of input parameters. key is parameter name, value is ApiParam class # dictionary of input parameters. key is parameter name, value is ApiParam class
@@ -336,6 +336,22 @@ class JsonHttpApiFunction(abc.ABC):
output[p] = p_class.decode(v) output[p] = p_class.decode(v)
return output return output
def rewrite_url(self, data: dict, url: str) -> Tuple[dict, str]:
"""
Rewrite a static URL using information passed in the data dict. This method may be overloaded by a derived
class to allow fully dynamic URLs. The input parameters required for the URL rewriting may be passed using
data parameter. In case those parameters are additional parameters that are not intended to be passed to
the encode_client method later, they must be removed explcitly.
Args:
data: (see JsonHttpApiClient and JsonHttpApiServer)
url: statically generated URL string (see comment in JsonHttpApiClient)
"""
# This implementation is a placeholder in which we do not perform any URL rewriting. We just pass through data
# and url unmodified.
return data, url
class JsonHttpApiClient(): class JsonHttpApiClient():
def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str], def __init__(self, api_func: JsonHttpApiFunction, url_prefix: str, func_req_id: Optional[str],
session: requests.Session): session: requests.Session):
@@ -352,8 +368,16 @@ class JsonHttpApiClient():
self.session = session self.session = session
def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]: def call(self, data: dict, func_call_id: Optional[str] = None, timeout=10) -> Optional[dict]:
"""Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as """
json-serializable dict. Output data is returned as json-deserialized dict.""" Make an API call to the HTTP API endpoint represented by this object. Input data is passed in `data` as
json-serializable fields. `data` may also contain additional parameters required for URL rewriting (see
rewrite_url in class JsonHttpApiFunction). Output data is returned as json-deserialized dict.
Args:
data: Input data required to perform the request.
func_call_id: Function Call Identifier, if present a header field is generated automatically.
timeout: Maximum amount of time to wait for the request to complete.
"""
# In case a function caller ID is supplied, use it together with the stored function requestor ID to generate # In case a function caller ID is supplied, use it together with the stored function requestor ID to generate
# and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header # and prepend the header field according to SGP.22, section 6.5.1.1 and 6.5.1.3. (the presence of the header
@@ -362,6 +386,11 @@ class JsonHttpApiClient():
data = {'header' : {'functionRequesterIdentifier': self.func_req_id, data = {'header' : {'functionRequesterIdentifier': self.func_req_id,
'functionCallIdentifier': func_call_id}} | data 'functionCallIdentifier': func_call_id}} | data
# The URL used for the HTTP request (see below) normally consists of the initially given url_prefix
# concatenated with the path defined by the JsonHttpApiFunction definition. This static URL path may be
# rewritten by rewrite_url method defined in the JsonHttpApiFunction.
data, url = self.api_func.rewrite_url(data, self.url_prefix + self.api_func.path)
# Encode the message (the presence of mandatory fields is checked during encoding) # Encode the message (the presence of mandatory fields is checked during encoding)
encoded = json.dumps(self.api_func.encode_client(data)) encoded = json.dumps(self.api_func.encode_client(data))
@@ -373,7 +402,6 @@ class JsonHttpApiClient():
req_headers.update(self.api_func.extra_http_req_headers) req_headers.update(self.api_func.extra_http_req_headers)
# Perform HTTP request # Perform HTTP request
url = self.url_prefix + self.api_func.path
logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded)) logger.debug("HTTP REQ %s - hdr: %s '%s'" % (url, req_headers, encoded))
response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout) response = self.session.request(self.api_func.http_method, url, data=encoded, headers=req_headers, timeout=timeout)
logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers)) logger.debug("HTTP RSP-STS: [%u] hdr: %s" % (response.status_code, response.headers))

View File

@@ -151,6 +151,8 @@ class File:
self.df_name = None self.df_name = None
self.fill_pattern = None self.fill_pattern = None
self.fill_pattern_repeat = False self.fill_pattern_repeat = False
self.pstdo = None # pinStatusTemplateDO, mandatory for DF/ADF
self.lcsi = None # optional life cycle status indicator
# apply some defaults from profile # apply some defaults from profile
if self.template: if self.template:
self.from_template(self.template) self.from_template(self.template)
@@ -278,6 +280,8 @@ class File:
elif self.file_type in ['MF', 'DF', 'ADF']: elif self.file_type in ['MF', 'DF', 'ADF']:
fdb_dec['file_type'] = 'df' fdb_dec['file_type'] = 'df'
fdb_dec['structure'] = 'no_info_given' fdb_dec['structure'] = 'no_info_given'
# pinStatusTemplateDO is mandatory for DF/ADF
fileDescriptor['pinStatusTemplateDO'] = self.pstdo
# build file descriptor based on above input data # build file descriptor based on above input data
fd_dict = {} fd_dict = {}
if len(fdb_dec): if len(fdb_dec):
@@ -304,6 +308,8 @@ class File:
# desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles # desired fill or repeat pattern in the "proprietaryEFInfo" element for the EF in Profiles
# downloaded to a V2.2 or earlier eUICC. # downloaded to a V2.2 or earlier eUICC.
fileDescriptor['proprietaryEFInfo'] = pefi fileDescriptor['proprietaryEFInfo'] = pefi
if self.lcsi:
fileDescriptor['lcsi'] = self.lcsi
logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor)) logger.debug("%s: to_fileDescriptor(%s)" % (self, fileDescriptor))
return fileDescriptor return fileDescriptor
@@ -323,6 +329,8 @@ class File:
if efFileSize: if efFileSize:
self._file_size = self._decode_file_size(efFileSize) self._file_size = self._decode_file_size(efFileSize)
self.pstdo = fileDescriptor.get('pinStatusTemplateDO', None)
self.lcsi = fileDescriptor.get('lcsi', None)
pefi = fileDescriptor.get('proprietaryEFInfo', {}) pefi = fileDescriptor.get('proprietaryEFInfo', {})
securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None) securityAttributesReferenced = fileDescriptor.get('securityAttributesReferenced', None)
if securityAttributesReferenced: if securityAttributesReferenced:

View File

@@ -63,7 +63,7 @@ class PySimLogger:
raise RuntimeError('static class, do not instantiate') raise RuntimeError('static class, do not instantiate')
@staticmethod @staticmethod
def setup(print_callback = None, colors:dict = {}, verbose_debug:bool = False): def setup(print_callback = None, colors:dict = {}):
""" """
Set a print callback function and color scheme. This function call is optional. In case this method is not Set a print callback function and color scheme. This function call is optional. In case this method is not
called, default settings apply. called, default settings apply.
@@ -72,20 +72,10 @@ class PySimLogger:
have the following format: print_callback(message:str) have the following format: print_callback(message:str)
colors : An optional dict through which certain log levels can be assigned a color. colors : An optional dict through which certain log levels can be assigned a color.
(e.g. {logging.WARN: YELLOW}) (e.g. {logging.WARN: YELLOW})
verbose_debug: Enable verbose logging and set the loglevel DEBUG when set to true. Otherwise the
non-verbose logging is used and the loglevel is set to INFO. This setting can be changed
using the set_verbose and set_level methods at any time.
""" """
PySimLogger.print_callback = print_callback PySimLogger.print_callback = print_callback
PySimLogger.colors = colors PySimLogger.colors = colors
if (verbose_debug):
PySimLogger.set_verbose(True)
PySimLogger.set_level(logging.DEBUG)
else:
PySimLogger.set_verbose(False)
PySimLogger.set_level(logging.INFO)
@staticmethod @staticmethod
def set_verbose(verbose:bool = False): def set_verbose(verbose:bool = False):
""" """

View File

@@ -301,53 +301,24 @@ class LinkBaseTpdu(LinkBase):
prev_tpdu = tpdu prev_tpdu = tpdu
data, sw = self.send_tpdu(tpdu) data, sw = self.send_tpdu(tpdu)
log.debug("T0: case #%u TPDU: %s => %s %s", case, tpdu, data or "(no data)", sw or "(no status word)")
# After sending the APDU/TPDU the UICC/eUICC or SIM may response with a status word that indicates that further # When we have sent the first APDU, the SW may indicate that there are response bytes
# TPDUs have to be sent in order to complete the task. # available. There are two SWs commonly used for this 9fxx (sim) and 61xx (usim), where
# xx is the number of response bytes available.
# See also:
if sw is not None: if sw is not None:
if case == 4 or self.apdu_strict == False: while (sw[0:2] in ['9f', '61', '62', '63']):
# In case the APDU is a case #4 APDU, the UICC/eUICC/SIM may indicate that there is response data # SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
# available which has to be retrieved using a GET RESPONSE command TPDU. # SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
# # SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
# ETSI TS 102 221, section 7.3.1.1.4 is very cleare about the fact that the GET RESPONSE mechanism tpdu_gr = tpdu[0:2] + 'c00000' + sw[2:4]
# shall only apply on case #4 APDUs but unfortunately it is impossible to distinguish between case #3 prev_tpdu = tpdu_gr
# and case #4 when the APDU format is not strictly followed. In order to be able to detect case #4 d, sw = self.send_tpdu(tpdu_gr)
# correctly the Le byte (usually 0x00) must be present, is often forgotten. To avoid problems with data += d
# legacy scripts that use raw APDU strings, we will still loosely apply GET RESPONSE based on what
# the status word indicates. Unless the user explicitly enables the strict mode (set apdu_strict true)
while True:
if sw in ['9000', '9100']:
# A status word of 9000 (or 9100 in case there is pending data from a proactive SIM command)
# indicates that either no response data was returnd or all response data has been retrieved
# successfully. We may discontinue the processing at this point.
break;
if sw[0:2] in ['61', '9f']:
# A status word of 61xx or 9fxx indicates that there is (still) response data available. We
# send a GET RESPONSE command with the length value indicated in the second byte of the status
# word. (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4a and 3GPP TS 51.011 9.4.1 and
# ISO/IEC 7816-4, Table 5)
le_gr = sw[2:4]
elif sw[0:2] in ['62', '63']:
# There are corner cases (status word is 62xx or 63xx) where the UICC/eUICC/SIM asks us
# to send a dummy GET RESPONSE command. We send a GET RESPONSE command with a length of 0.
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4b and ETSI TS 151 011, section 9.4.1)
le_gr = '00'
else:
# A status word other then the ones covered by the above logic may indicate an error. In this
# case we will discontinue the processing as well.
# (see also ETSI TS 102 221, section 7.3.1.1.4, clause 4c)
break
tpdu_gr = tpdu[0:2] + 'c00000' + le_gr
prev_tpdu = tpdu_gr
data_gr, sw = self.send_tpdu(tpdu_gr)
log.debug("T0: GET RESPONSE TPDU: %s => %s %s", tpdu_gr, data_gr or "(no data)", sw or "(no status word)")
data += data_gr
if sw[0:2] == '6c': if sw[0:2] == '6c':
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding # SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
tpdu_gr = prev_tpdu[0:8] + sw[2:4] tpdu_gr = prev_tpdu[0:8] + sw[2:4]
data, sw = self.send_tpdu(tpdu_gr) data, sw = self.send_tpdu(tpdu_gr)
log.debug("T0: repated case #%u TPDU: %s => %s %s", case, tpdu_gr, data or "(no data)", sw or "(no status word)")
return data, sw return data, sw

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: Fairwaves-SIM Autodetected card type: Fairwaves-SIM
ICCID: 8988219000000117833 ICCID: 8988219000000117833

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: Wavemobile-SIM Autodetected card type: Wavemobile-SIM
ICCID: 89445310150011013678 ICCID: 89445310150011013678

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: fakemagicsim Autodetected card type: fakemagicsim
ICCID: 1122334455667788990 ICCID: 1122334455667788990

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmoISIM-SJA2 Autodetected card type: sysmoISIM-SJA2
ICCID: 8988211000000467343 ICCID: 8988211000000467343

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmoISIM-SJA5 Autodetected card type: sysmoISIM-SJA5
ICCID: 8949440000001155314 ICCID: 8949440000001155314

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmoUSIM-SJS1 Autodetected card type: sysmoUSIM-SJS1
ICCID: 8988211320300000028 ICCID: 8988211320300000028

View File

@@ -1,4 +1,4 @@
INFO: Using PC/SC reader interface Using PC/SC reader interface
Reading ... Reading ...
Autodetected card type: sysmosim-gr1 Autodetected card type: sysmosim-gr1
ICCID: 2222334455667788990 ICCID: 2222334455667788990