scp81: rename passthru to redirect, add a true passthru mode (v2.2.13)
The former 'passthru' mode is now 'redirect': it pins the configured target and every BIP channel is connected there (the card's requested address is only logged; host/port required). The name 'passthru' now means the new mode: no listener and no target - each channel dials the destination the card requests in OPEN CHANNEL (Other address + Transport level port, TCP client, remote, 0x02 only). The specs define no default port (TS 102 223 8.59), so an incomplete or non-TCP request fails the channel with result 3A and an open-fail log reason. BipTerminal gains a mode (enable(host, port, mode=...), open(..., proto=...)), reports it in status(), and the control API/status expose redirect (target) and passthru (per-channel targets). The PWA mode selector shows four modes with per-mode notes and disables Host/Port in passthru; the status line shows each channel's actual target. Docs, help (EN/RU), READMEs and the AGENTS notes updated; SW cache otaman-v179. Breaking API change: mode:'passthru' has the new semantics - use mode:'redirect' for the previous behavior (no alias).
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
"""HTTP OTA (SCP81 / GP RAM over HTTP) emulation.
|
||||
|
||||
Phase A: terminal-side BIP emulation (OPEN/SEND/RECEIVE/CLOSE CHANNEL) plus a
|
||||
raw TCP capture listener. The card's BIP channel is always redirected to the
|
||||
locally configured target (the future PSK TLS platform); the address the card
|
||||
requested is only logged.
|
||||
raw TCP capture listener. In the default redirect mode the card's BIP channel
|
||||
is always redirected to the locally configured target (the future PSK TLS
|
||||
platform) and the address the card requested is only logged; in passthru mode
|
||||
the channel dials the destination the card requests in OPEN CHANNEL (TCP,
|
||||
UICC in client mode, remote connection).
|
||||
|
||||
Reference behavior (TS 102 223 8.52-8.56, GP v2.2 Amendment B) is taken from
|
||||
the captured real-terminal traces in samples/HTTP_OTA/traces:
|
||||
@@ -154,6 +156,7 @@ class BipTerminal:
|
||||
|
||||
def __init__(self):
|
||||
self.enabled = False
|
||||
self.mode = 'redirect'
|
||||
self.target = None
|
||||
self.channels = {}
|
||||
self.next_id = 1
|
||||
@@ -213,10 +216,17 @@ class BipTerminal:
|
||||
name='bip-monitor', daemon=True)
|
||||
self._monitor.start()
|
||||
|
||||
def enable(self, host, port):
|
||||
self.target = (host, int(port))
|
||||
def enable(self, host=None, port=None, mode='redirect'):
|
||||
"""Enable the BIP terminal.
|
||||
|
||||
'redirect' (default) pins one target: every channel goes there whatever
|
||||
address the card requests. 'passthru' has no target at all: every
|
||||
channel dials the destination the card requested in OPEN CHANNEL."""
|
||||
self.mode = mode if mode in ('redirect', 'passthru') else 'redirect'
|
||||
self.target = (host, int(port)) if host and port not in (None, '') else None
|
||||
self.enabled = True
|
||||
self.log('enabled', target='%s:%d' % self.target)
|
||||
self.log('enabled', mode=self.mode,
|
||||
target='%s:%d' % self.target if self.target else None)
|
||||
self._start_monitor()
|
||||
|
||||
def disable(self):
|
||||
@@ -271,12 +281,40 @@ class BipTerminal:
|
||||
return cid
|
||||
return None
|
||||
|
||||
def open(self, requested_host, requested_port, buffer_size):
|
||||
"""Open a channel to the redirect target. Returns (channel_id, error)."""
|
||||
if not self.enabled or not self.target:
|
||||
def open(self, requested_host, requested_port, buffer_size, proto=None):
|
||||
"""Open a channel.
|
||||
|
||||
Redirect modes connect to the pinned target; passthru dials the
|
||||
destination the card sent in OPEN CHANNEL (Other address + Transport
|
||||
level port). Returns (channel_id, error)."""
|
||||
if not self.enabled:
|
||||
return None, 'bip disabled'
|
||||
target = self.target
|
||||
requested = '%s:%s' % (requested_host, requested_port)
|
||||
if self.mode == 'passthru':
|
||||
# Use the card's request as-is: TCP, UICC in client mode, remote
|
||||
# connection (TS 102 223 6.4.27.2 / 8.59). The specs define no
|
||||
# default port, so an incomplete or non-TCP request fails.
|
||||
host = (requested_host or '').strip()
|
||||
try:
|
||||
port = int(requested_port)
|
||||
except (TypeError, ValueError):
|
||||
port = 0
|
||||
if proto != 0x02:
|
||||
reason = 'card did not request TCP client transport (passthru)'
|
||||
elif not host or host == '-':
|
||||
reason = 'card did not request a destination address (passthru)'
|
||||
elif not 0 < port <= 0xFFFF:
|
||||
reason = 'card did not request a valid port (passthru)'
|
||||
else:
|
||||
reason = None
|
||||
if reason:
|
||||
self.log('open-fail', requested=requested, reason=reason)
|
||||
return None, reason
|
||||
target = (host, port)
|
||||
else:
|
||||
if not self.target:
|
||||
return None, 'bip disabled'
|
||||
target = self.target
|
||||
cid = self._alloc_id()
|
||||
if cid is None:
|
||||
self.log('open-fail', requested=requested, reason='no free channel')
|
||||
@@ -363,6 +401,7 @@ class BipTerminal:
|
||||
})
|
||||
return {
|
||||
'enabled': self.enabled,
|
||||
'mode': self.mode,
|
||||
'target': '%s:%d' % self.target if self.target else None,
|
||||
'channels': channels,
|
||||
'seq': self.seq,
|
||||
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.2.12'
|
||||
VERSION = '2.2.13'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -851,9 +851,12 @@ _PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES}
|
||||
|
||||
_BIP = httpota.BipTerminal()
|
||||
_SCP81_LISTENER = None
|
||||
# Active listener mode and target: ('dump'|'tls'|'passthru', host, port).
|
||||
# passthru has no listener object - the BIP channels connect straight to the
|
||||
# external platform - so the mode/target are tracked here for the status API.
|
||||
# Active listener mode: 'dump' | 'tls' | 'redirect' | 'passthru'.
|
||||
# 'redirect' pins one target and has no listener object - the BIP channels
|
||||
# connect straight to the configured external platform (TLS terminated
|
||||
# there). 'passthru' has neither listener nor target: each channel dials the
|
||||
# destination the card requests in OPEN CHANNEL. Mode/target are tracked here
|
||||
# for the status API.
|
||||
_SCP81_MODE = None
|
||||
_SCP81_TARGET = None
|
||||
# PSK table of the TLS listener: identity -> key (memory only, never logged or
|
||||
@@ -1409,7 +1412,7 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
# destination TLVs - see the AGENTS.md HTTP OTA notes).
|
||||
_BIP.log('open-relaxed', address=addr, port=port,
|
||||
note='destination/transport not fully specified')
|
||||
cid, err = _BIP.open(addr or '-', port or 0, buffer_size)
|
||||
cid, err = _BIP.open(addr or '-', port or 0, buffer_size, proto=proto)
|
||||
if cid is None:
|
||||
return _bip_tr(cmd_num, cmd_type, cmd_qual, dev_src, dev_dst, 0x3A, 0x00, extra)
|
||||
if cmd_qual and (cmd_qual & 0x04):
|
||||
@@ -1461,10 +1464,15 @@ def _handle_bip_command(scc, cmd_num, cmd_type, cmd_qual, raw, dev_src, dev_dst)
|
||||
|
||||
def _scp81_listener_status():
|
||||
if not _SCP81_LISTENER:
|
||||
if _SCP81_MODE == 'passthru' and _SCP81_TARGET:
|
||||
return {'mode': 'passthru', 'host': _SCP81_TARGET[0],
|
||||
if _SCP81_MODE == 'redirect' and _SCP81_TARGET:
|
||||
return {'mode': 'redirect', 'host': _SCP81_TARGET[0],
|
||||
'port': _SCP81_TARGET[1],
|
||||
'target': '%s:%d' % _SCP81_TARGET}
|
||||
if _SCP81_MODE == 'passthru':
|
||||
# No listener and no pinned target: every channel dials the
|
||||
# destination the card requests (per-channel targets in the
|
||||
# BIP status).
|
||||
return {'mode': 'passthru'}
|
||||
return None
|
||||
if isinstance(_SCP81_LISTENER, scp81.PskTlsServer):
|
||||
return {'mode': 'tls', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port,
|
||||
@@ -2010,18 +2018,28 @@ def _scp81_bip_control(body):
|
||||
_SCP81_LISTENER.stop()
|
||||
_SCP81_LISTENER = None
|
||||
_BIP.disable()
|
||||
if mode == 'passthru':
|
||||
# No local listener: the card's BIP channels connect straight to the
|
||||
# external platform (e.g. a production HTTP OTA server), which
|
||||
# terminates TLS and runs the administration dialog. The target is a
|
||||
# configured address, never the address the card requests.
|
||||
if mode == 'redirect':
|
||||
# No local listener: the card's BIP channels are redirected straight
|
||||
# to the configured target (e.g. a production HTTP OTA server), which
|
||||
# terminates TLS and runs the administration dialog. The address the
|
||||
# card requests is only logged.
|
||||
if not body.get('host') or body.get('port') in (None, ''):
|
||||
return {'ok': False,
|
||||
'error': 'passthru mode requires the target host and port'}
|
||||
_SCP81_MODE = 'passthru'
|
||||
'error': 'redirect mode requires the target host and port'}
|
||||
_SCP81_MODE = 'redirect'
|
||||
_SCP81_TARGET = (host, port)
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(host, port)
|
||||
_BIP.enable(host, port, mode='redirect')
|
||||
return {'ok': True, 'bip': _BIP.status(),
|
||||
'listener': _scp81_listener_status()}
|
||||
if mode == 'passthru':
|
||||
# No local listener and no pinned target: every BIP channel dials the
|
||||
# destination the card requests in OPEN CHANNEL (Other address +
|
||||
# Transport level port, TCP client only). Host and port are unused.
|
||||
_SCP81_MODE = 'passthru'
|
||||
_SCP81_TARGET = None
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(mode='passthru')
|
||||
return {'ok': True, 'bip': _BIP.status(),
|
||||
'listener': _scp81_listener_status()}
|
||||
if mode == 'tls':
|
||||
@@ -2103,7 +2121,7 @@ def _scp81_bip_control(body):
|
||||
_SCP81_MODE = 'tls'
|
||||
_SCP81_TARGET = (_SCP81_LISTENER.host, _SCP81_LISTENER.port)
|
||||
_BIP.on_data = _bip_data_available
|
||||
_BIP.enable(host, _SCP81_LISTENER.port)
|
||||
_BIP.enable(host, _SCP81_LISTENER.port, mode='redirect')
|
||||
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status(),
|
||||
'script': list(_SCP81_SCRIPT_BASE),
|
||||
'script_kind': _SCP81_SCRIPT_KIND,
|
||||
@@ -2121,7 +2139,7 @@ def _scp81_bip_control(body):
|
||||
on_log=lambda kind, **fields: _BIP.log(kind, **fields))
|
||||
_SCP81_MODE = 'dump'
|
||||
_SCP81_TARGET = (_SCP81_LISTENER.host, _SCP81_LISTENER.port)
|
||||
_BIP.enable(host, _SCP81_LISTENER.port)
|
||||
_BIP.enable(host, _SCP81_LISTENER.port, mode='redirect')
|
||||
return {'ok': True, 'bip': _BIP.status(), 'listener': _scp81_listener_status()}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user