scp81: always reuse the connection, collapsible options, label polish (v2.2.15)
- Keep-alive is no longer a setting: the TLS connection stays open between POSTs for the whole dialog (the card, as HTTP client, may still dial a new one at any time - GP Am. B 4.3.1) and only the 204 ends it, with a clean close_notify while the response is still buffered, then FIN. Dropped keep_alive/on_before_close/_scp81_wait_drained and the Connection-header 'close' value (the API, status and UI no longer carry a keep-alive knob). - The Listener Options block is a collapsed <details> with a 'custom' badge when anything differs from the reference defaults; the Reset button moved into the body so it cannot toggle the panel. - X-Admin-Targeted-Application is opt-in (checkbox + //aid/... field, field disabled while off); X-Admin-Next-URI has a checkbox + hint explaining the one-shot rule of GP Am. B 4.4.2; both dependent fields grey out when unchecked. - Labels/i18n: 'Chunked body (Transfer-Encoding: chunked)' stays English, 'Show link events (...)', 'Теги comprehension-required'; the compact-header wording now spells out that it omits the optional space after ':' (legal per RFC 7230 3.2 OWS; saves one byte per header). - Tests: a 200 keeps the socket for the next POST; the 204 closes with a mutual close_notify exchange; options helper/badge unit tests; removed the close-per-response and drain-wait tests. SW cache otaman-v181.
This commit is contained in:
@@ -132,8 +132,8 @@ class PskTlsServer:
|
||||
|
||||
def __init__(self, host, port, psk=None, identity=None, on_log=None,
|
||||
responder=None, timeout=10.0, chunked=False, chunk_size=0,
|
||||
keep_alive=False, compact_headers=False, tls_version='auto',
|
||||
cipher=None, on_before_close=None, keylog=None,
|
||||
compact_headers=False, tls_version='auto',
|
||||
cipher=None, keylog=None,
|
||||
conn_header=None, half_close=False, answer_delay=0.0,
|
||||
psk_map=None):
|
||||
# PSK lookup table: identity -> key. With an explicit psk_map a
|
||||
@@ -159,7 +159,6 @@ class PskTlsServer:
|
||||
self.chunked = chunked
|
||||
# chunk_size 0 = one record for the whole response
|
||||
self.chunk_size = int(chunk_size)
|
||||
self.keep_alive = keep_alive
|
||||
self.compact_headers = compact_headers
|
||||
# TLS is permissive by default: 'auto' accepts TLS 1.0-1.2 and lets
|
||||
# OpenSSL pick the highest the card offers. The '1.0'/'1.1'/'1.2'
|
||||
@@ -170,17 +169,13 @@ class PskTlsServer:
|
||||
# Pin one cipher suite (e.g. PSK-AES128-CBC-SHA) if the card's SD only
|
||||
# maps a specific suite to a usable SCP81 security level.
|
||||
self.cipher = cipher or None
|
||||
# Called with the peer address just before closing a non-keep-alive
|
||||
# connection: the server waits until the card has drained the BIP
|
||||
# buffer, otherwise the EOF truncates the response fetch.
|
||||
self.on_before_close = on_before_close
|
||||
# Debug aid: write the TLS traffic secrets to this file
|
||||
# (SSLKEYLOGFILE format), so captures of the PSK dialog can be
|
||||
# decrypted (tshark etc). Contains key material - use a temp path.
|
||||
self.keylog = keylog or None
|
||||
# Connection header value: None = auto ('keep-alive'/'close' per the
|
||||
# keep_alive flag), 'none' = omit the header (implicit HTTP/1.1
|
||||
# keep-alive).
|
||||
# Connection header value: None/'none' = omit the header (implicit
|
||||
# HTTP/1.1 keep-alive); 'keep-alive' adds it explicitly. The server
|
||||
# never closes mid-session - only the 204 ends the dialog.
|
||||
self.conn_header = conn_header or None
|
||||
# TLS half-close after a script body. NOTE (live 2026-09-16):
|
||||
# CPython's SSLSocket.unwrap() poisons the session when the peer does
|
||||
@@ -367,11 +362,7 @@ class PskTlsServer:
|
||||
status, resp_headers, resp_body = self.responder(
|
||||
method, target, headers, body)
|
||||
reason = {200: 'OK', 204: 'No Content'}.get(status, 'Status')
|
||||
conn_hdr = self.conn_header
|
||||
if conn_hdr == 'none':
|
||||
conn_hdr = None
|
||||
elif conn_hdr is None:
|
||||
conn_hdr = 'keep-alive' if self.keep_alive else 'close'
|
||||
conn_hdr = None if self.conn_header in (None, 'none') else self.conn_header
|
||||
response = build_http_response(
|
||||
status, reason, resp_headers, resp_body,
|
||||
chunked=self.chunked, compact=self.compact_headers,
|
||||
@@ -393,38 +384,23 @@ class PskTlsServer:
|
||||
bytes=len(resp_body), chunked=self.chunked,
|
||||
response_hex=response.hex().upper()[:600],
|
||||
body_hex=resp_body.hex().upper()[:2000] or None)
|
||||
# 204 always ends the dialog. Without keep-alive every response
|
||||
# ends it: the card's HTTP client appears to delimit the
|
||||
# response at connection close (live 2026-09-15) and then
|
||||
# starts a fresh session for its next POST.
|
||||
if status == 204 or not resp_body or not self.keep_alive:
|
||||
peer_name = None
|
||||
if resp_body and self.on_before_close:
|
||||
try:
|
||||
peer_name = tls.getpeername()
|
||||
except Exception:
|
||||
peer_name = None
|
||||
# Only the end of the dialog closes the connection: 204 (or
|
||||
# an empty body) ends the session; every other response
|
||||
# leaves the TLS connection open for the card's next POST.
|
||||
# Reusing it - or dialing a fresh one - is the card's call
|
||||
# (GP Am. B 4.3.1: the SD manages connection establishment).
|
||||
if status == 204 or not resp_body:
|
||||
# Clean TLS shutdown with the response still in the BIP
|
||||
# buffer: the card fetches the 204 and the close_notify
|
||||
# together, then the FIN. A bare close here makes the
|
||||
# card abort the session with a fatal alert.
|
||||
plain = None
|
||||
if not self.keep_alive:
|
||||
# Clean TLS shutdown BEFORE the card drains the
|
||||
# buffer: a bare TCP close leaves the card's TLS stack
|
||||
# with a truncated session (it then neither processes
|
||||
# the script nor posts the response), and a
|
||||
# close_notify sent only after the drain is never
|
||||
# fetched. Send it while the response still waits, so
|
||||
# the card reads both, then wait for the buffer to
|
||||
# drain and only then send the FIN.
|
||||
try:
|
||||
tls.settimeout(2.0)
|
||||
plain = tls.unwrap()
|
||||
tls = None
|
||||
except Exception:
|
||||
plain = None
|
||||
if peer_name and self.on_before_close:
|
||||
try:
|
||||
self.on_before_close(peer_name)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
tls.settimeout(2.0)
|
||||
plain = tls.unwrap()
|
||||
tls = None
|
||||
except Exception:
|
||||
plain = None
|
||||
if plain is not None:
|
||||
try:
|
||||
plain.close()
|
||||
|
||||
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '2.2.14'
|
||||
VERSION = '2.2.15'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1484,41 +1484,12 @@ def _scp81_listener_status():
|
||||
'cipher_seen': _SCP81_LISTENER.cipher_seen,
|
||||
'chunked': _SCP81_LISTENER.chunked,
|
||||
'chunk_size': _SCP81_LISTENER.chunk_size,
|
||||
'keep_alive': _SCP81_LISTENER.keep_alive,
|
||||
'compact_headers': _SCP81_LISTENER.compact_headers,
|
||||
'tls_version': _SCP81_LISTENER.tls_version,
|
||||
'cipher': _SCP81_LISTENER.cipher}
|
||||
return {'mode': 'dump', 'host': _SCP81_LISTENER.host, 'port': _SCP81_LISTENER.port}
|
||||
|
||||
|
||||
def _scp81_wait_drained(peer):
|
||||
"""Wait until the BIP channel for this TLS connection has delivered its
|
||||
buffered bytes to the card (matched by the terminal's ephemeral port), so
|
||||
a connection close does not truncate the response fetch."""
|
||||
if not peer or len(peer) < 2:
|
||||
return
|
||||
port = peer[1]
|
||||
deadline = time.time() + 5.0
|
||||
seen_data = False
|
||||
while time.time() < deadline:
|
||||
ch = None
|
||||
for c in list(_BIP.channels.values()):
|
||||
try:
|
||||
if c.sock.getsockname()[1] == port:
|
||||
ch = c
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if ch is None:
|
||||
return
|
||||
if ch.rx:
|
||||
# Channel pump has picked up the response; wait for the card.
|
||||
seen_data = True
|
||||
elif seen_data:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
|
||||
|
||||
def _bip_data_available(ch):
|
||||
"""Monitor-thread callback: tell the card there is server data to fetch.
|
||||
|
||||
@@ -2094,11 +2065,9 @@ def _scp81_bip_control(body):
|
||||
responder=_scp81_script_responder,
|
||||
chunked=bool(body.get('chunked', True)),
|
||||
chunk_size=chunk_size,
|
||||
keep_alive=bool(body.get('keep_alive', True)),
|
||||
compact_headers=bool(body.get('compact_headers', False)),
|
||||
tls_version=str(body.get('tls_version') or 'auto'),
|
||||
cipher=(body.get('cipher') or None),
|
||||
on_before_close=_scp81_wait_drained,
|
||||
keylog=(body.get('keylog') or None),
|
||||
conn_header=(body.get('conn_header') or 'none'),
|
||||
answer_delay=(body.get('answer_delay') or 0),
|
||||
|
||||
Reference in New Issue
Block a user