v3.29.0: the batch kinds (delete / reinstall) + the delete-package pre-step
The batch plan is a list of named steps ({name, apdu, delete}) now and
_ram_install_run generalized into _ram_plan_run; POST /api/ram-batch/start
dispatches on kind:
- install-cap (default; delete_package prepends a cascade DELETE on the
package AID - a leftover package blocks INSTALL [for load]),
- reinstall-app (_ram_reinstall_plan: DELETE the applet instance + INSTALL
[for install] from the loaded package - no .cap, no LOAD),
- delete (_ram_delete_plan: one GP DELETE, aid + target app/package ->
P2=00/P2=80).
A delete step tolerates 'not found' (6A88/6A82 - the object is already
gone, the goal is met); any other delete failure stops the chain with the
delete's reason. The policy serves all three kinds and the single
/api/ram-install-app, which now deletes the instance first (make
selectable stays one step); /api/ram-install takes delete_package too.
The status/summary carry the run's kind (the panel labels follow:
installed/deleted/reinstalled) and each attempt carries the last three
steps (steps + steps_total) for the panel's per-step detail. The PWA's
three batch entries are grouped at the end of the operation selector (an
optgroup); ramIsBatchOp() is the one source for "per-card batch".
Docs: help EN/RU, api.md, the repo AGENTS. Tests: the plan builders per
kind, the validations, the delete-package prepend, the not-found
tolerance, the kind dispatch/status/notice, the steps tail, the endpoint
steps, the PWA wiring. 925 frontend + 943 Python; sw cache simple-v447.
This commit is contained in:
+223
-149
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.28.8'
|
||||
VERSION = '3.29.0'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -935,6 +935,21 @@ def _cap_make_selectable_apdu(instance_aid, privileges='00'):
|
||||
return '80E60800%02X%s' % (len(data) // 2, data)
|
||||
|
||||
|
||||
def _ram_delete_apdu(aid, cascade=False):
|
||||
"""GP DELETE (Card Spec v2.3.1 11.2, Tables 11-20/11-23): P1=00 (last/only
|
||||
command), P2.b8 selects 'object' ('00') or 'object and related objects'
|
||||
('80'), the data field carries the mandatory '4F' AID TLV. Case 3 (no
|
||||
trailing Le: on the card's SCP80 layer a trailing byte counts as a phantom
|
||||
second command whose SW masks the real result). The PWA's ramDeleteApdu
|
||||
(the Explore buttons) is the mirror; both suites pin the same bytes."""
|
||||
a = re.sub(r'[^0-9a-fA-F]', '', str(aid or '')).upper()
|
||||
if not a:
|
||||
return ''
|
||||
data = '4F' + _ber_len(len(a) // 2) + a
|
||||
return ('80E400' + ('80' if cascade else '00')
|
||||
+ _ber_len(len(data) // 2) + data).upper()
|
||||
|
||||
|
||||
def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='',
|
||||
privileges='00', install_params='', stk_params='',
|
||||
make_selectable=True, block_size=240, instance_aid=None,
|
||||
@@ -1614,6 +1629,12 @@ def _send_secured_packet(scc, sp_hex, oa_number, sm_sc=None, include_cpi=True,
|
||||
# GlobalPlatform "more data available" (GET STATUS pages).
|
||||
_RAM_REMOTE_SW_OK = ('9000', 'CAFE')
|
||||
|
||||
# A GP DELETE whose object is already gone: the delete's goal is met, so a
|
||||
# delete step (the batch delete, the reinstall's instance delete, the
|
||||
# delete-package pre-step) tolerates these and the chain proceeds (6A88 =
|
||||
# referenced data not found; 6A82 = file/application not found).
|
||||
_RAM_DELETE_NOT_FOUND = ('6A88', '6A82')
|
||||
|
||||
|
||||
def _ram_remote_sw_ok(sw):
|
||||
s = str(sw or '').upper()
|
||||
@@ -2401,9 +2422,11 @@ def _ram_nv_fields(nv_before, nv_after):
|
||||
def _ram_install_plan(body):
|
||||
"""The card-independent part of a CAP install: parse the .cap, build the
|
||||
INSTALL [for load] -> LOAD xN -> INSTALL [for install] chain and read the
|
||||
transport knobs. Nothing here touches the card, so the batch builds the
|
||||
plan once and reuses it for every card. Raises ValueError with the
|
||||
caller's message (the endpoint and the batch answer 400)."""
|
||||
transport knobs. `delete_package` prepends a cascade DELETE on the package
|
||||
AID (a re-install over a leftover package; the delete step tolerates 'not
|
||||
found' - see _ram_plan_run). Nothing here touches the card, so the batch
|
||||
builds the plan once and reuses it for every card. Raises ValueError with
|
||||
the caller's message (the endpoint and the batch answer 400)."""
|
||||
cap_hex = str(body.get('cap_hex') or '').replace(' ', '')
|
||||
if not cap_hex:
|
||||
raise ValueError('No cap_hex provided')
|
||||
@@ -2439,21 +2462,102 @@ def _ram_install_plan(body):
|
||||
block_size=block_size,
|
||||
nv_quota=body.get('nv_quota'),
|
||||
volatile_quota=body.get('volatile_quota'))
|
||||
return {'loadfile_aid': loadfile_aid, 'module_aid': module_aid, 'seq': seq,
|
||||
'block_size': block_size, 'block_size_req': block_size_req,
|
||||
steps = []
|
||||
for i, apdu in enumerate(seq):
|
||||
if i == 0:
|
||||
name = 'INSTALL [for load]'
|
||||
elif i == len(seq) - 1:
|
||||
name = 'INSTALL [for install]'
|
||||
else:
|
||||
name = 'LOAD (%d/%d)' % (i, len(seq) - 2)
|
||||
steps.append({'name': name, 'apdu': apdu})
|
||||
if body.get('delete_package'):
|
||||
# A leftover package blocks INSTALL [for load]: the cascade delete
|
||||
# clears the ELF, its modules and its applications first. It is a
|
||||
# delete step, so 'not found' (a fresh card) lets the chain proceed.
|
||||
steps.insert(0, {'name': 'DELETE (package + applications)',
|
||||
'apdu': _ram_delete_apdu(loadfile_aid, cascade=True),
|
||||
'delete': True})
|
||||
return {'kind': 'install-cap', 'seq': steps,
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format'))}
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format')),
|
||||
'nv': True, 'load_size': block_size,
|
||||
'response': {'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_auto': not block_size_req}}
|
||||
|
||||
|
||||
def _ram_install_run(server, body, plan, scc):
|
||||
"""Run one CAP install chain on `scc` - the caller holds the card lock.
|
||||
def _ram_delete_plan(body):
|
||||
"""The batch delete plan: one GP DELETE step. `target` selects the P2
|
||||
semantics (GP Card Spec v2.3.1 11.2): 'app' deletes the object only (an
|
||||
applet instance, P2=00), 'package' the object and its related objects (the
|
||||
load file with its modules and installed applications, P2=80). Raises
|
||||
ValueError (400 at Start)."""
|
||||
aid = re.sub(r'[^0-9a-fA-F]', '', str(body.get('aid') or '')).upper()
|
||||
if not re.fullmatch(r'(?:[0-9A-F]{2}){5,16}', aid):
|
||||
raise ValueError('aid must be 5-16 bytes of hex (an AID)')
|
||||
target = str(body.get('target') or 'app').strip().lower()
|
||||
if target not in ('app', 'package'):
|
||||
raise ValueError("target must be 'app' or 'package'")
|
||||
cascade = target == 'package'
|
||||
return {'kind': 'delete',
|
||||
'seq': [{'name': 'DELETE (package + applications)' if cascade
|
||||
else 'DELETE (application)',
|
||||
'apdu': _ram_delete_apdu(aid, cascade), 'delete': True}],
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format')),
|
||||
'response': {'aid': aid, 'target': target}}
|
||||
|
||||
The per-card part: the preset's ISD TAR supplies the keyset (v3.26.0) and
|
||||
the counter is seeded from the store (v3.20.0). Raises ValueError for a
|
||||
request error (the endpoint and the batch answer 400 / record the skip); a
|
||||
chain failure comes back as ``{'success': False, ...}``. Returns the
|
||||
response dict. Shared by /api/ram-install and the batch worker."""
|
||||
|
||||
def _ram_reinstall_plan(body, kind='reinstall-app'):
|
||||
"""The reinstall plan: DELETE the applet instance (tolerating 'not found' -
|
||||
a fresh card just installs), then INSTALL [for install] from the already
|
||||
loaded package. No .cap and no LOAD: the same parameters the single
|
||||
INSTALL [for install] uses, validated here (400 at Start, never per card).
|
||||
`kind` is 'install-app' for the single operation, 'reinstall-app' for the
|
||||
batch (only the progress/log label differs)."""
|
||||
loadfile = str(body.get('loadfile_aid') or '').replace(' ', '').upper()
|
||||
module = str(body.get('module_aid') or '').replace(' ', '').upper()
|
||||
if not loadfile or not module:
|
||||
raise ValueError('loadfile_aid and module_aid required')
|
||||
instance = (str(body.get('instance_aid') or '').replace(' ', '').upper()
|
||||
or module)
|
||||
install_apdu = _cap_install_apdu(
|
||||
loadfile, module, instance_aid=instance,
|
||||
privileges=str(body.get('privileges') or '').replace(' ', '') or '00',
|
||||
install_params=str(body.get('install_params') or '').replace(' ', ''),
|
||||
stk_params=str(body.get('stk_params') or '').replace(' ', ''),
|
||||
make_selectable=body.get('make_selectable', True),
|
||||
nv_quota=body.get('nv_quota'),
|
||||
volatile_quota=body.get('volatile_quota'))
|
||||
return {'kind': kind,
|
||||
'seq': [{'name': 'DELETE (application)',
|
||||
'apdu': _ram_delete_apdu(instance, cascade=False),
|
||||
'delete': True},
|
||||
{'name': 'INSTALL [for install]', 'apdu': install_apdu}],
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format')),
|
||||
'response': {'load_file_aid': loadfile, 'module_aid': module,
|
||||
'instance_aid': instance}}
|
||||
|
||||
|
||||
def _ram_plan_run(server, body, plan, scc):
|
||||
"""Run one batch plan on `scc` - the caller holds the card lock.
|
||||
|
||||
A plan is a list of named steps (an INSTALL/LOAD chain, GP DELETEs, a
|
||||
single INSTALL [for install]); a step flagged `delete` tolerates the 'not
|
||||
found' status words (6A88/6A82 - the object is already gone, so the
|
||||
delete's goal is met and the chain proceeds), while any other failure stops
|
||||
it. The per-card part: the preset's ISD TAR supplies the keyset (v3.26.0)
|
||||
and the counter is seeded from the store (v3.20.0). Raises ValueError for
|
||||
a request error (the endpoint and the batch answer 400 / record the skip);
|
||||
a chain failure comes back as ``{'success': False, ...}``. Returns the
|
||||
response dict. Shared by /api/ram-install, /api/ram-install-app and the
|
||||
batch worker."""
|
||||
preset = _preset_by_id(server, body.get('preset_id'))
|
||||
tar = str(body.get('tar') or presets.role_tar(preset, 'isd')
|
||||
or '000000').strip().upper()
|
||||
@@ -2475,11 +2579,12 @@ def _ram_install_run(server, body, plan, scc):
|
||||
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
||||
'include_cpi': plan['include_cpi']}
|
||||
seq = plan['seq']
|
||||
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes\n' % plan['block_size'])
|
||||
kind = plan.get('kind') or 'ram'
|
||||
label = 'RAM-%s' % kind.upper().replace('-', ' ')
|
||||
|
||||
# Live progress for the PWA's operation modal (the request holds _CARD_LOCK
|
||||
# until the chain is done; /api/status carries this)
|
||||
_ram_progress_begin('install-cap', len(seq) + 1)
|
||||
_ram_progress_begin(kind, len(seq) + 1)
|
||||
nv_before = None
|
||||
nv_after = None
|
||||
try:
|
||||
@@ -2489,55 +2594,58 @@ def _ram_install_run(server, body, plan, scc):
|
||||
if ram_format == 'auto':
|
||||
_ram_progress_step(0, 'FORMAT CHECK')
|
||||
ram_format = _ram_detect_format(server, scc, sp_state, state)
|
||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
|
||||
len(seq), len(seq) - 2, ram_format, plan['loadfile_aid']))
|
||||
if plan.get('load_size'):
|
||||
sys.stderr.write('RAM-INSTALL: LOAD block size %d bytes\n' % plan['load_size'])
|
||||
sys.stderr.write('%s: %d step(s), %s\n' % (label, len(seq), ram_format))
|
||||
# NV footprint: free non-volatile memory before the chain (best effort
|
||||
# - a card without FF21 skips it).
|
||||
nv_before = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV before')
|
||||
if plan.get('nv'):
|
||||
nv_before = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV before')
|
||||
|
||||
for apdu_idx, gp_apdu in enumerate(seq):
|
||||
if apdu_idx == 0:
|
||||
step_name = 'INSTALL [for load]'
|
||||
elif apdu_idx == len(seq) - 1:
|
||||
step_name = 'INSTALL [for install]'
|
||||
else:
|
||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||
_ram_progress_step(apdu_idx + 1, step_name)
|
||||
if not _ram_send_gp_apdu(server, scc, sp_state, state, step_name,
|
||||
_ram_format_apdu(gp_apdu, ram_format)):
|
||||
for step_idx, step in enumerate(seq):
|
||||
step_name = step['name']
|
||||
_ram_progress_step(step_idx + 1, step_name)
|
||||
ok = _ram_send_gp_apdu(server, scc, sp_state, state, step_name,
|
||||
_ram_format_apdu(step['apdu'], ram_format))
|
||||
if not ok and step.get('delete'):
|
||||
# A delete step tolerates 'not found': the object is already
|
||||
# gone (6A88/6A82), which is exactly what the delete wanted -
|
||||
# the chain proceeds. Any other failure stops it.
|
||||
rec = steps[-1] if steps else {}
|
||||
sw = str(rec.get('por_sw') or '').upper()
|
||||
if sw in _RAM_DELETE_NOT_FOUND:
|
||||
rec['delete_not_found'] = True
|
||||
ok = True
|
||||
if not ok:
|
||||
# What the failed attempt consumed (a rejected load may keep or
|
||||
# discard its partial file - the delta tells which).
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state,
|
||||
ram_format, 'NV after')
|
||||
if plan.get('nv'):
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state,
|
||||
ram_format, 'NV after')
|
||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||
'error': (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name)),
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format,
|
||||
'load_file_aid': plan['loadfile_aid'],
|
||||
'module_aid': plan['module_aid'],
|
||||
'load_block_size': plan['block_size'],
|
||||
'load_block_size_requested': plan['block_size_req'],
|
||||
'load_block_size_auto': not plan['block_size_req']}
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format}
|
||||
resp.update(plan.get('response') or {})
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
failed_rec = steps[-1] if steps else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
if resp.get('cntr_low'):
|
||||
sys.stderr.write('RAM-INSTALL: low counter - the packet '
|
||||
'counter is not above the card\'s\n')
|
||||
sys.stderr.write('%s: low counter - the packet counter is '
|
||||
'not above the card\'s\n' % label)
|
||||
return resp
|
||||
# NV footprint: what the successful chain consumed.
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV after')
|
||||
if plan.get('nv'):
|
||||
nv_after = _ram_read_ff21(server, scc, sp_state, state, ram_format, 'NV after')
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
|
||||
resp = {'success': True, 'steps': steps, 'load_file_aid': plan['loadfile_aid'],
|
||||
'module_aid': plan['module_aid'], 'final_cntr': state['cntr'],
|
||||
'ram_format': ram_format, 'load_block_size': plan['block_size'],
|
||||
'load_block_size_requested': plan['block_size_req'],
|
||||
'load_block_size_auto': not plan['block_size_req']}
|
||||
resp = {'success': True, 'steps': steps, 'final_cntr': state['cntr'],
|
||||
'ram_format': ram_format}
|
||||
resp.update(plan.get('response') or {})
|
||||
resp.update(_ram_nv_fields(nv_before, nv_after))
|
||||
sys.stderr.write('RAM-INSTALL: Complete — loadfile_aid=%s module_aid=%s cntr=%s\n' % (
|
||||
plan['loadfile_aid'], plan['module_aid'], state['cntr']))
|
||||
sys.stderr.write('%s: Complete (%d step(s), cntr=%s)\n' % (
|
||||
label, len(seq), state['cntr']))
|
||||
return resp
|
||||
|
||||
|
||||
@@ -2587,7 +2695,12 @@ def _ram_batch_record(state, rec):
|
||||
state['counts'][result] += 1
|
||||
who = rec.get('iccid') or 'the card'
|
||||
if result == 'installed':
|
||||
state['notice'] = 'Card %s installed.' % who
|
||||
# the success verb follows the operation (the counts bucket is
|
||||
# 'installed' for every kind - "the operation succeeded")
|
||||
kind = ((state.get('plan') or {}).get('kind')) or 'install-cap'
|
||||
verb = {'delete': 'deleted',
|
||||
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
|
||||
state['notice'] = 'Card %s %s.' % (who, verb)
|
||||
elif result == 'failed':
|
||||
step = rec.get('failed_step') or ''
|
||||
sw = rec.get('por_sw') or rec.get('sw') or ''
|
||||
@@ -2615,6 +2728,20 @@ def _ram_batch_failure_fields(result):
|
||||
'sw': rec.get('sw') or ''}
|
||||
|
||||
|
||||
def _ram_batch_steps_fields(result):
|
||||
"""The attempt's per-step verdict tail for the panel: the last three steps
|
||||
(name, ok, remote SW; a delete step that answered 'not found' is ok) plus
|
||||
the chain length - a full LOAD chain stays short in the report."""
|
||||
steps = result.get('steps') or []
|
||||
out = []
|
||||
for s in steps[-3:]:
|
||||
out.append({'name': s.get('name') or '',
|
||||
'ok': (not s.get('por_error')) or bool(s.get('delete_not_found')),
|
||||
'sw': s.get('por_sw') or '',
|
||||
'not_found': bool(s.get('delete_not_found'))})
|
||||
return {'steps': out, 'steps_total': len(steps)}
|
||||
|
||||
|
||||
def _ram_batch_handle_card(state, server):
|
||||
"""One equipped card: match the preset by ICCID, check its ISD TAR keyset
|
||||
and run the install under the card lock. A card already attempted in this
|
||||
@@ -2655,7 +2782,7 @@ def _ram_batch_handle_card(state, server):
|
||||
request_error = False
|
||||
try:
|
||||
with _CARD_LOCK:
|
||||
result = _ram_install_run(server, body, state['plan'], server.scc)
|
||||
result = _ram_plan_run(server, body, state['plan'], server.scc)
|
||||
except ValueError as e:
|
||||
# A request/eligibility error (the preset's ISD keyset, the plan): the
|
||||
# install never started - the card is skipped.
|
||||
@@ -2678,6 +2805,7 @@ def _ram_batch_handle_card(state, server):
|
||||
'load_file_aid': result.get('load_file_aid'),
|
||||
'module_aid': result.get('module_aid'),
|
||||
'nv_delta': result.get('nv_delta')}
|
||||
rec.update(_ram_batch_steps_fields(result))
|
||||
elif result is not None:
|
||||
outcome = 'failed'
|
||||
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
||||
@@ -2685,6 +2813,7 @@ def _ram_batch_handle_card(state, server):
|
||||
'cntr_low': bool(result.get('cntr_low')),
|
||||
'error': result.get('error') or ''}
|
||||
rec.update(_ram_batch_failure_fields(result))
|
||||
rec.update(_ram_batch_steps_fields(result))
|
||||
elif request_error:
|
||||
outcome = 'skipped'
|
||||
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
||||
@@ -2750,23 +2879,35 @@ def _ram_batch_worker(state):
|
||||
|
||||
|
||||
def _ram_batch_start(body):
|
||||
"""Validate the CAP/parameters (the plan is card independent, so it is built
|
||||
once) and spawn the worker (the worker reads `_server_ref` itself). Raises ValueError (400) for a bad request and
|
||||
RuntimeError (409) when another batch or a test run is active."""
|
||||
"""Validate the operation's parameters (the plan is card independent, so it
|
||||
is built once) and spawn the worker (the worker reads `_server_ref` itself).
|
||||
`kind` selects the operation: 'install-cap' (the .cap chain, optionally with
|
||||
the delete-package pre-step), 'delete' (one GP DELETE) or 'reinstall-app'
|
||||
(DELETE the instance + INSTALL [for install]). Raises ValueError (400) for
|
||||
a bad request and RuntimeError (409) when another batch or a test run is
|
||||
active."""
|
||||
global _RAM_BATCH
|
||||
with _RAM_BATCH_LOCK:
|
||||
if _RAM_BATCH and _RAM_BATCH.get('active'):
|
||||
raise RuntimeError('a batch is already running - stop it first')
|
||||
if _TEST_RUNNING:
|
||||
raise RuntimeError('a test run is active - stop it first')
|
||||
plan = _ram_install_plan(body)
|
||||
kind = str(body.get('kind') or 'install-cap').strip().lower()
|
||||
if kind == 'install-cap':
|
||||
plan = _ram_install_plan(body)
|
||||
elif kind == 'delete':
|
||||
plan = _ram_delete_plan(body)
|
||||
elif kind == 'reinstall-app':
|
||||
plan = _ram_reinstall_plan(body)
|
||||
else:
|
||||
raise ValueError("kind must be 'install-cap', 'delete' or 'reinstall-app'")
|
||||
state = _ram_batch_new_state(plan)
|
||||
with _RAM_BATCH_LOCK:
|
||||
_RAM_BATCH = state
|
||||
threading.Thread(target=_ram_batch_worker, args=(state,),
|
||||
name='ram-batch', daemon=True).start()
|
||||
sys.stderr.write('RAM-BATCH: started (%d APDUs, loadfile_aid=%s)\n'
|
||||
% (len(plan['seq']), plan['loadfile_aid']))
|
||||
sys.stderr.write('RAM-BATCH: started (%s, %d step(s))\n'
|
||||
% (plan['kind'], len(plan['seq'])))
|
||||
return state
|
||||
|
||||
|
||||
@@ -2800,9 +2941,11 @@ def _ram_batch_status():
|
||||
return {'active': False, 'phase': 'idle', 'attempts': [],
|
||||
'attempts_total': 0, 'current': None, 'notice': '',
|
||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
||||
'started': None, 'stopped': None, 'progress': None}
|
||||
'started': None, 'stopped': None, 'progress': None,
|
||||
'kind': None}
|
||||
return {'active': bool(_RAM_BATCH.get('active')),
|
||||
'phase': _RAM_BATCH.get('phase'),
|
||||
'kind': ((_RAM_BATCH.get('plan') or {}).get('kind')) or None,
|
||||
'attempts': list(_RAM_BATCH['attempts']),
|
||||
'attempts_total': _RAM_BATCH['attempts_total'],
|
||||
'current': dict(_RAM_BATCH['current']) if _RAM_BATCH.get('current') else None,
|
||||
@@ -2823,6 +2966,7 @@ def _ram_batch_summary():
|
||||
cur = _RAM_BATCH.get('current')
|
||||
return {'active': bool(_RAM_BATCH.get('active')),
|
||||
'phase': _RAM_BATCH.get('phase'),
|
||||
'kind': ((_RAM_BATCH.get('plan') or {}).get('kind')) or None,
|
||||
'current': dict(cur) if cur else None,
|
||||
'notice': _RAM_BATCH.get('notice') or '',
|
||||
'attempts_total': _RAM_BATCH['attempts_total'],
|
||||
@@ -10040,7 +10184,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
# then one run on the equipped card - both shared with the
|
||||
# batch worker (v3.28.0).
|
||||
plan = _ram_install_plan(body)
|
||||
resp = _ram_install_run(self.server, body, plan, scc)
|
||||
resp = _ram_plan_run(self.server, body, plan, scc)
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
@@ -10118,107 +10262,37 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
return
|
||||
try:
|
||||
mode = (body.get('mode') or 'install').strip()
|
||||
loadfile_aid = (body.get('loadfile_aid') or '').replace(' ', '').upper()
|
||||
module_aid = (body.get('module_aid') or '').replace(' ', '').upper()
|
||||
instance_aid = ((body.get('instance_aid') or '').replace(' ', '').upper()
|
||||
or module_aid)
|
||||
privileges = (body.get('privileges') or '').replace(' ', '') or '00'
|
||||
install_params_hex = (body.get('install_params') or '').replace(' ', '')
|
||||
stk_params_hex = (body.get('stk_params') or '').replace(' ', '')
|
||||
make_selectable = bool(body.get('make_selectable', True))
|
||||
if mode == 'make_selectable':
|
||||
instance_aid = ((body.get('instance_aid') or '')
|
||||
.replace(' ', '').upper())
|
||||
if not instance_aid:
|
||||
err = {'success': False, 'error': 'instance_aid required'}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
apdu = _cap_make_selectable_apdu(instance_aid, privileges)
|
||||
step_name = 'INSTALL [for make selectable]'
|
||||
privileges = (body.get('privileges') or '').replace(' ', '') or '00'
|
||||
plan = {'kind': 'make-selectable',
|
||||
'seq': [{'name': 'INSTALL [for make selectable]',
|
||||
'apdu': _cap_make_selectable_apdu(instance_aid,
|
||||
privileges)}],
|
||||
'spi2': str(body.get('spi2') or '01'),
|
||||
'include_cpi': body.get('includeCpi', True),
|
||||
'ram_format': _ram_normalize_format(body.get('ram_format'))}
|
||||
elif mode == 'install':
|
||||
# The install deletes the applet instance first (tolerating
|
||||
# 'not found') so re-installing with new parameters works in
|
||||
# one step; the plan builder validates the AIDs and the
|
||||
# parameter composition (400 here, never mid-chain).
|
||||
plan = _ram_reinstall_plan(body, kind='install-app')
|
||||
else:
|
||||
if not loadfile_aid or not module_aid:
|
||||
err = {'success': False,
|
||||
'error': 'loadfile_aid and module_aid required'}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
try:
|
||||
apdu = _cap_install_apdu(
|
||||
loadfile_aid, module_aid, instance_aid=instance_aid,
|
||||
privileges=privileges, install_params=install_params_hex,
|
||||
stk_params=stk_params_hex, make_selectable=make_selectable,
|
||||
nv_quota=body.get('nv_quota'),
|
||||
volatile_quota=body.get('volatile_quota'))
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
step_name = 'INSTALL [for install]'
|
||||
# SCP80 params: the SPI1 comes from the MSL of the packet's
|
||||
# TAR (the operation uses the preset's table); a caller-supplied
|
||||
# value is only the fallback for a TAR the preset does not carry.
|
||||
preset = _preset_by_id(self.server, body.get('preset_id'))
|
||||
tar = str(body.get('tar') or presets.role_tar(preset, 'isd')
|
||||
or '000000').strip().upper()
|
||||
try:
|
||||
spi1, _ = _spi1_for_tar(preset, tar, body.get('spi1'),
|
||||
prefer_msl=True)
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
sp_state = {
|
||||
'spi1': spi1, 'spi2': body.get('spi2', '01'),
|
||||
'tar': tar, 'cntr': body.get('cntr', '00000000'),
|
||||
'include_cpi': body.get('includeCpi', True)}
|
||||
# The keyset: the preset's default for the operation's TAR
|
||||
# (v3.26.0) unless the request names KIc/KID itself - then the
|
||||
# number must be consistent and defined in the preset
|
||||
# (TS 102 225 A.2 / the "keyset must exist" rule) - checked
|
||||
# before the format probe consumes a counter.
|
||||
kvn, kic, kid, keyset_err = self._op_keyset(body, tar)
|
||||
if keyset_err:
|
||||
err = {'success': False, 'error': keyset_err}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
return
|
||||
sp_state['kic'] = kic
|
||||
sp_state['kid'] = kid
|
||||
sp_state['kic_key'] = body.get('kicKey', '')
|
||||
sp_state['kid_key'] = body.get('kidKey', '')
|
||||
# The counter: explicit `cntr` wins, else the preset's value
|
||||
# for this keyset (the server owns the counters, v3.20.0).
|
||||
sp_state['cntr'] = _preset_counter_seed(self.server, body, kvn) or sp_state['cntr']
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||
'cntr': sp_state['cntr'], 'preset_id': body.get('preset_id'),
|
||||
'kvn': kvn}
|
||||
# Live progress for the PWA's operation modal (see /api/ram-install)
|
||||
_ram_progress_begin('install-app', 2)
|
||||
try:
|
||||
# RAM command format: detected per operation (a read-only
|
||||
# probe step) unless the caller pinned compact/expanded.
|
||||
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||
if ram_format == 'auto':
|
||||
_ram_progress_step(0, 'FORMAT CHECK')
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
apdu = _ram_format_apdu(apdu, ram_format)
|
||||
_ram_progress_step(1, step_name)
|
||||
sys.stderr.write('RAM-INSTALL-APP: %s (%s, %s) cntr=%s\n' % (
|
||||
apdu, step_name, ram_format, sp_state['cntr']))
|
||||
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
resp = {'success': bool(ok), 'steps': state['steps'],
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format}
|
||||
if not ok:
|
||||
resp['error'] = (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name))
|
||||
resp['failed_step'] = len(state['steps'])
|
||||
failed_rec = state['steps'][-1] if state['steps'] else {}
|
||||
resp.update(_cntr_low_fields(failed_rec.get('por_status')))
|
||||
raise ValueError("mode must be 'install' or 'make_selectable'")
|
||||
resp = _ram_plan_run(self.server, body, plan, scc)
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
except ValueError as e:
|
||||
err = {'success': False, 'error': str(e)}
|
||||
self._send_json(err, 400)
|
||||
self._log_resp(err)
|
||||
except Exception as e:
|
||||
sys.stderr.write('RAM-INSTALL-APP error: %s\n' % e)
|
||||
err = {'success': False, 'error': str(e)}
|
||||
|
||||
Reference in New Issue
Block a user