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
3 changed files with 42 additions and 6 deletions

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: