fix: v3.8.0 preset-store review findings
- /api/send-ota no longer 500s on a counter-less body: the plain `sp` path
reports final_cntr only when the counter is usable (_counter_valid, 1-10 hex
digits) - before, the new plain-path report ran int('') and the request died
in the generic handler. The failure path is guarded the same way.
- _decode_por wraps the keyset/SPI construction in the same "never raise" rule
as the decode: a malformed counter or missing keys return None instead of
escaping the handler.
- presets.add() never accepts a taken id (a fresh uuid is assigned, like the
import path), the counter is validated as 1-10 hex digits instead of being
silently masked/kept, and set_counter() ignores a malformed value with a log
line instead of writing it.
- the preset endpoints answer a JSON 500 when the store write fails (read-only
home, disk full) instead of dropping the connection.
- tests: counter-less send-ota, store-write 500, duplicate id, counter
validation (add/update/set_counter), and the temp-file check no longer
matches the audit file; docs/api.md documents final_cntr for the plain path
and preset_id.
This commit is contained in:
@@ -1412,6 +1412,13 @@ def _ram_next_cntr(cntr, advance):
|
||||
return '%010X' % ((int(cntr, 16) + 1) % (2 ** 40))
|
||||
|
||||
|
||||
def _counter_valid(cntr):
|
||||
"""True when an SCP80 counter is usable (1-10 hex digits). A caller
|
||||
sending a pre-built packet may have no counter at all - the empty value
|
||||
must never reach int() (v3.8.0 review fix)."""
|
||||
return bool(re.fullmatch(r'[0-9A-Fa-f]{1,10}', str(cntr or '')))
|
||||
|
||||
|
||||
def _preset_counter_persist(server, preset_id, cntr, source):
|
||||
"""Persist the counter an operation accepted into the server-side preset
|
||||
store (v3.8.0). The counter is card state, so the server writes it with
|
||||
@@ -1736,8 +1743,13 @@ def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, respon
|
||||
from osmocom.utils import h2b, b2h
|
||||
if not response_hex:
|
||||
return None
|
||||
otak = _ota_keyset(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex)
|
||||
spi = _spi_from_bytes(int(spi1, 16), int(spi2, 16))
|
||||
try:
|
||||
otak = _ota_keyset(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex)
|
||||
spi = _spi_from_bytes(int(spi1, 16), int(spi2, 16))
|
||||
except Exception:
|
||||
# a malformed counter/key (e.g. a hand-made request) is not a PoR
|
||||
# either - the same rule as the decode below: never raise here.
|
||||
return None
|
||||
try:
|
||||
data = h2b(response_hex)
|
||||
if not data or data[0] != 0x02:
|
||||
@@ -5634,6 +5646,13 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
return None
|
||||
return store
|
||||
|
||||
def _preset_store_error(self, e):
|
||||
"""A store I/O failure (read-only home, disk full, ...) answers a JSON
|
||||
500 instead of escaping the handler as a dropped connection."""
|
||||
resp = {'error': 'preset store write failed: %s' % e}
|
||||
self._send_json(resp, 500)
|
||||
self._log_resp(resp)
|
||||
|
||||
def _log_req(self, body=None):
|
||||
if self.server.log_requests:
|
||||
if body is not None:
|
||||
@@ -6777,6 +6796,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
resp = result
|
||||
if ram_format is not None:
|
||||
resp['ram_format'] = ram_format
|
||||
if _counter_valid(cntr):
|
||||
resp['final_cntr'] = cntr
|
||||
# A warning ENVELOPE may still carry the PoR (the
|
||||
# card's counter verdict): decode it so the caller
|
||||
@@ -6853,7 +6873,10 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
resp['ram_format'] = ram_format
|
||||
# The counter to use next, reported for the RAM and the
|
||||
# plain SCP80 path alike; the server persists it (v3.8.0).
|
||||
resp['final_cntr'] = _ram_next_cntr(cntr, accepted)
|
||||
# A pre-built packet may come without a counter - then
|
||||
# nothing is reported (and int('') never happens).
|
||||
if _counter_valid(cntr):
|
||||
resp['final_cntr'] = _ram_next_cntr(cntr, accepted)
|
||||
finally:
|
||||
if submit_handler and hasattr(scc, '_tp'):
|
||||
scc._tp.proactive_handler = old_proactive
|
||||
@@ -7384,6 +7407,9 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(resp, 400)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
except OSError as e:
|
||||
self._preset_store_error(e)
|
||||
return
|
||||
resp = {'ok': True, 'preset': preset}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
@@ -7403,6 +7429,9 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(resp, 400)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
except OSError as e:
|
||||
self._preset_store_error(e)
|
||||
return
|
||||
if preset is None:
|
||||
resp = {'error': 'unknown preset id'}
|
||||
self._send_json(resp, 404)
|
||||
@@ -7417,7 +7446,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
store = self._preset_store_or_503()
|
||||
if store is None:
|
||||
return
|
||||
removed = store.remove(body.get('id'))
|
||||
try:
|
||||
removed = store.remove(body.get('id'))
|
||||
except OSError as e:
|
||||
self._preset_store_error(e)
|
||||
return
|
||||
resp = {'ok': True, 'removed': removed}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
@@ -7434,7 +7467,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._log_resp(resp)
|
||||
return
|
||||
mode = body.get('mode') if body.get('mode') in ('merge', 'replace') else 'merge'
|
||||
resp = store.import_presets(items, mode)
|
||||
try:
|
||||
resp = store.import_presets(items, mode)
|
||||
except OSError as e:
|
||||
self._preset_store_error(e)
|
||||
return
|
||||
resp['ok'] = True
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
|
||||
Reference in New Issue
Block a user