295f5553fc
A headless/SSH box can show a world-writable /run/pcscd/pcscd.comm (srw-rw-rw- root root) and still be denied: pcscd enforces its PolicyKit policy, whose defaults allow only active local sessions. The startup report, the start.sh pre-flight and the README troubleshooting now name the exact rule (99-pcscd-allow.rules, org.debian.pcsc-lite.access_pcsc and access_card) and explain why the socket mode does not matter. The pcscd-group instruction is gone: there may be no such group, and it is not the gate. The group route stays only as a note for a pcscd built without PolicyKit. Tests: tests/test_pcsc_startup.py asserts the polkit action IDs, the rule filename and the 0666-socket explanation. 1088 Python + 1075 frontend.
554 lines
28 KiB
Python
554 lines
28 KiB
Python
import argparse
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
import traceback
|
|
from http.server import ThreadingHTTPServer
|
|
from pySim.card_handler import CardHandler
|
|
from pySim.commands import SimCardCommands
|
|
from pySim.exceptions import NoCardError, ReaderError
|
|
from pySim.log import PySimLogger
|
|
from pySim.cards import UiccCardBase
|
|
|
|
from .shell import load_pysim_app
|
|
from . import fastinit
|
|
from . import gsmtap
|
|
from . import presets
|
|
from . import test_scripts
|
|
from . import test_suites
|
|
from . import testscript
|
|
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _read_arr, _read_pbr, _netstate_read, _netstate_install, _LineFilter, _test_command_type, migrate_scripts_to_suites, _is_pcsc_error, _clear_app_card_state
|
|
|
|
|
|
_server_start = 0
|
|
|
|
# PC/SC hresults (pyscard's exceptions carry them; the messages are often
|
|
# empty, so the hresult is the reliable signal).
|
|
_PCSC_E_UNKNOWN_READER = 0x80100009
|
|
_PCSC_E_NO_SERVICE = 0x8010001D
|
|
_PCSC_E_SERVICE_STOPPED = 0x8010001E
|
|
_PCSC_E_NO_ACCESS = 0x80100027
|
|
_PCSC_E_NO_READERS_AVAILABLE = 0x8010002E
|
|
|
|
|
|
class PcscUnavailable(RuntimeError):
|
|
"""The PC/SC stack cannot be used (kind: access/service/reader)."""
|
|
|
|
def __init__(self, kind, cause=None):
|
|
super().__init__(kind)
|
|
self.kind = kind
|
|
self.cause = cause
|
|
|
|
|
|
def _pcsc_error_kind(exc):
|
|
"""Classify a pyscard failure: 'access', 'service', 'reader' or None.
|
|
|
|
``SCARD_E_NO_ACCESS`` is the "Access denied" a user without permission to
|
|
pcscd gets; the message is a fallback because some pyscard paths raise a
|
|
CardConnectionException with an empty message and only the hresult."""
|
|
hr = getattr(exc, 'hresult', None)
|
|
msg = str(exc or '').lower()
|
|
if hr == _PCSC_E_NO_ACCESS or 'access denied' in msg or 'access is denied' in msg:
|
|
return 'access'
|
|
if hr in (_PCSC_E_NO_SERVICE, _PCSC_E_SERVICE_STOPPED) or \
|
|
'resource manager is not running' in msg or 'service not available' in msg:
|
|
return 'service'
|
|
if hr in (_PCSC_E_UNKNOWN_READER, _PCSC_E_NO_READERS_AVAILABLE) or \
|
|
'no smart card readers' in msg:
|
|
return 'reader'
|
|
return None
|
|
|
|
|
|
def _report_pcsc_unavailable(kind, exc=None):
|
|
"""Print an actionable reason instead of starting a readerless server."""
|
|
if kind == 'access':
|
|
user = os.environ.get('USER') or os.environ.get('LOGNAME') or '$USER'
|
|
sys.stderr.write('INIT: PC/SC access denied - the current user may not use pcscd.\n')
|
|
sys.stderr.write(' pcscd gates access through PolicyKit: the default policy allows\n')
|
|
sys.stderr.write(' only *active local* sessions, so an SSH/headless session is denied\n')
|
|
sys.stderr.write(' even when /run/pcscd/pcscd.comm is world-writable (0666).\n')
|
|
sys.stderr.write(' Allow the user in /etc/polkit-1/rules.d/99-pcscd-allow.rules:\n')
|
|
sys.stderr.write(' polkit.addRule(function(a, s) {\n')
|
|
sys.stderr.write(' if ((a.id == "org.debian.pcsc-lite.access_pcsc" ||\n')
|
|
sys.stderr.write(' a.id == "org.debian.pcsc-lite.access_card") &&\n')
|
|
sys.stderr.write(' s.user == "%s") return polkit.Result.YES;\n' % user)
|
|
sys.stderr.write(' });\n')
|
|
sys.stderr.write(' then restart polkit (sudo systemctl restart polkit) or log out/in;\n')
|
|
sys.stderr.write(' `pcsc_scan` shows the same "SCardEstablishContext: Access denied"\n')
|
|
sys.stderr.write(' error until the rule is active.\n')
|
|
elif kind == 'service':
|
|
sys.stderr.write('INIT: pcscd is not running or not reachable.\n')
|
|
sys.stderr.write(' Start it (sudo systemctl start pcscd) and restart the server.\n')
|
|
else:
|
|
sys.stderr.write('INIT: no PC/SC reader found.\n')
|
|
sys.stderr.write(' Connect a reader (pcsc_scan lists them) and restart the server.\n')
|
|
if exc is not None:
|
|
hr = getattr(exc, 'hresult', None)
|
|
detail = str(exc) or type(exc).__name__
|
|
if hr not in (None, -1):
|
|
detail += ' (hresult=0x%08X)' % hr
|
|
sys.stderr.write(' (%s)\n' % detail)
|
|
|
|
|
|
def _probe_pcsc_reader(retries=3, sleep_fn=time.sleep, readers_fn=None):
|
|
"""Return the first PC/SC reader name or raise PcscUnavailable.
|
|
|
|
``SCARD_E_NO_ACCESS``/service failures are raised immediately (retrying
|
|
cannot fix permissions or a stopped service); an empty list is retried
|
|
(USB enumeration) and reported as 'reader' after the last attempt."""
|
|
if readers_fn is None:
|
|
from smartcard.System import readers as readers_fn
|
|
last = None
|
|
for attempt in range(retries):
|
|
try:
|
|
found = readers_fn()
|
|
except Exception as exc:
|
|
last = exc
|
|
kind = _pcsc_error_kind(exc)
|
|
if kind in ('access', 'service'):
|
|
raise PcscUnavailable(kind, exc) from exc
|
|
else:
|
|
if found:
|
|
return str(found[0])
|
|
last = None
|
|
if attempt + 1 < retries:
|
|
sys.stderr.write('INIT: no PC/SC reader yet, retrying in 2s...\n')
|
|
sleep_fn(2)
|
|
raise PcscUnavailable('reader', last)
|
|
|
|
|
|
def _serial_arg_given(argv=None):
|
|
"""True when the operator explicitly chose the serial reader (-d/--device).
|
|
|
|
The default (-d is /dev/ttyUSB0 in pySim's parser) must not masquerade as
|
|
an explicit choice: only a flag on the command line counts."""
|
|
argv = sys.argv[1:] if argv is None else argv
|
|
for arg in argv:
|
|
if arg in ('-d', '--device') or arg.startswith('--device='):
|
|
return True
|
|
if arg.startswith('-d') and not arg.startswith('--') and len(arg) > 2:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _log_stdout(msg):
|
|
elapsed = time.time() - _server_start
|
|
os.write(1, ('[%8.3f] %s\n' % (elapsed, msg)).encode())
|
|
|
|
|
|
def _guard_console_streams():
|
|
"""Never let a non-encodable character kill the server.
|
|
|
|
On Windows the launcher pipes stdout/stderr into PowerShell (see start.bat),
|
|
so Python encodes with the ANSI code page instead of the wide console API -
|
|
cp1251 cannot encode the banner's box-drawing rule (U+2500) or the APDU
|
|
trace arrows (U+2192), and one such character raised UnicodeEncodeError and
|
|
killed the process. errors="replace" keeps every message, whatever the code
|
|
page (a character the code page lacks prints as '?')."""
|
|
for stream in (sys.stdout, sys.stderr):
|
|
try:
|
|
stream.reconfigure(errors="replace")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _console_rule(n=70):
|
|
"""The banner rule in the widest character the output stream can encode.
|
|
|
|
Under start.bat stdout is a pipe (not a console), so Python encodes with the
|
|
ANSI code page - cp1251 (Russian Windows) cannot encode the box-drawing
|
|
U+2500 and falls back to a plain ASCII rule (cp437 has the box-drawing
|
|
characters and keeps the pretty one)."""
|
|
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
|
try:
|
|
"\u2500".encode(enc)
|
|
return "\u2500" * n
|
|
except (UnicodeEncodeError, LookupError):
|
|
return "-" * n
|
|
|
|
|
|
def _default_web_dir():
|
|
# <repo>/frontend, whether run from source or an editable install.
|
|
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend')
|
|
|
|
|
|
def _default_mcc_mnc_list():
|
|
# Bundled default: the MIT-licensed operator list ships with the package
|
|
# (data/mcc-mnc-list.json, from pbakondy/mcc-mnc-list); --mcc-mnc-list
|
|
# overrides it and the endpoint reports available=false when missing.
|
|
pkg = os.path.dirname(os.path.abspath(__file__))
|
|
return os.path.join(pkg, 'data', 'mcc-mnc-list.json')
|
|
|
|
|
|
def _build_http_server(host, port, handler):
|
|
"""HTTP server for the PWA + API.
|
|
|
|
Threaded on purpose: card access is serialized by ``_CARD_LOCK`` (all
|
|
POSTs, the card-touching GETs and the background threads take it), so
|
|
while a long operation holds the lock the cached endpoints and static
|
|
files must still be answerable - a single-threaded server starved the
|
|
RAM modal's live progress poll (`/api/status.ram_progress`) and showed
|
|
the whole install result only at the end. Daemon threads keep a stuck
|
|
request from blocking shutdown."""
|
|
return ThreadingHTTPServer((host, port), handler)
|
|
|
|
|
|
def main():
|
|
global _server_start
|
|
_guard_console_streams()
|
|
_server_start = time.time()
|
|
mod = load_pysim_app()
|
|
parser = mod.option_parser
|
|
parser.description = 'pysim-simple-server — HTTP API for pysim'
|
|
parser.add_argument('--http-host', default='127.0.0.1', help='Bind address (default: 127.0.0.1)')
|
|
parser.add_argument('--http-port', type=int, default=8080, help='Bind port (default: 8080)')
|
|
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
|
|
help='Directory with the SIMple PWA static files to serve (default: <repo>/frontend)')
|
|
parser.add_argument('--mcc-mnc-list', default=_default_mcc_mnc_list(), metavar='PATH',
|
|
help='Worldwide MCC/MNC operator list (JSON) for the network-simulation operator picker (default: the bundled data/mcc-mnc-list.json)')
|
|
parser.add_argument('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
|
|
parser.add_argument('--card-presets', default=None, metavar='PATH',
|
|
help='Card preset store (default: ~/.pysim-simple-server/card_presets.json)')
|
|
parser.add_argument('--test-scripts', default=None, metavar='PATH',
|
|
help='Test script store (default: ~/.pysim-simple-server/test_scripts.json)')
|
|
parser.add_argument('--test-suites', default=None, metavar='PATH',
|
|
help='Test suite store (default: ~/.pysim-simple-server/test_suites.json)')
|
|
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
|
|
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
|
|
parser.add_argument('--sms-sm-sc', default='12345678912', metavar='DIGITS',
|
|
help='SM-SC address for SMS-SUBMIT routing in PoR-in-submit mode (default: 12345678912)')
|
|
parser.add_argument('--terminal-profile',
|
|
default='FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003',
|
|
metavar='HEX',
|
|
help='TERMINAL PROFILE payload (default: the 33-byte profile of a real BIP-capable handset - the live card only starts HTTP OTA when BIP events/commands are advertised)')
|
|
parser.add_argument('--poll-interval', type=int, default=30, metavar='SECS',
|
|
help='Idle interval before automatic STATUS polling (1-255 seconds, default: 30). Disable with --poll-interval 0')
|
|
parser.add_argument('--no-card-init', action='store_true', default=False,
|
|
help='Skip pysim card initialization (preserve CAT session — no file manager)')
|
|
parser.add_argument('--timing', action='store_true', default=False,
|
|
help='Log phase durations, card resets and APDU counters with elapsed timestamps')
|
|
parser.add_argument('--fast-init', action='store_true', help=argparse.SUPPRESS)
|
|
parser.add_argument('--full-pysim-init', action='store_true', default=False,
|
|
help="Use pysim's stock init_card/equip (multiple physical card resets) instead of the default reset-free fast init")
|
|
parser.add_argument('--menu-timeout', type=int, default=60, metavar='SECS',
|
|
help='Auto-send a timeout TERMINAL RESPONSE if a paused STK command is not answered (default: 60, 0 disables)')
|
|
parser.add_argument('--no-auto-equip', action='store_true', default=False,
|
|
help='Do not automatically initialize a card right after it is inserted (default: auto-equip on)')
|
|
parser.add_argument('--gsmtap', nargs='?', const=gsmtap.DEFAULT_TARGET, default=None,
|
|
metavar='HOST[:PORT]',
|
|
help='Stream every APDU (and the card ATR) as GSMTAP-SIM UDP packets for '
|
|
'Wireshark / SIMtrace Analyser (default target: %s)' % gsmtap.DEFAULT_TARGET)
|
|
|
|
opts = parser.parse_args()
|
|
opts.skip_card_init = opts.no_card_init
|
|
opts.fast_init = not opts.full_pysim_init
|
|
if opts.timing:
|
|
_timing_on()
|
|
if opts.menu_timeout is not None:
|
|
_set_menu_timeout(opts.menu_timeout)
|
|
set_auto_equip(not opts.no_auto_equip and not opts.skip_card_init)
|
|
# APDU tracers: --apdu-trace (stderr) and --gsmtap (GSMTAP-SIM UDP) can be
|
|
# combined; the fan-out keeps pySim's single-tracer interface.
|
|
gsmtap_sender = None
|
|
tracers = []
|
|
if opts.apdu_trace:
|
|
tracers.append(_LoggingApduTracer())
|
|
if opts.gsmtap is not None:
|
|
try:
|
|
gsmtap_host, gsmtap_port = gsmtap.parse_target(opts.gsmtap)
|
|
gsmtap_sender = gsmtap.GsmtapSender(gsmtap_host, gsmtap_port)
|
|
tracers.append(gsmtap.GsmtapApduTracer(gsmtap_sender))
|
|
sys.stderr.write('GSMTAP: streaming APDUs to %s\n' % gsmtap_sender.target)
|
|
except (ValueError, OSError) as e:
|
|
sys.stderr.write('GSMTAP: disabled (%s)\n' % e)
|
|
tracer = None
|
|
if len(tracers) == 1:
|
|
tracer = tracers[0]
|
|
elif tracers:
|
|
tracer = gsmtap.FanoutApduTracer(tracers)
|
|
sl = None
|
|
scc = None
|
|
card = None
|
|
rs = None
|
|
sim_menu = None
|
|
event_list = None
|
|
# Auto-detect the PC/SC reader unless one was named explicitly or the
|
|
# operator chose the serial transport (-d). A PC/SC access denial or a
|
|
# missing service must be reported - silently falling back to the serial
|
|
# reader left the server "listening" without a usable card link (v3.38.1).
|
|
if opts.pcsc_dev is None and opts.pcsc_regex is None and not _serial_arg_given():
|
|
try:
|
|
reader = _probe_pcsc_reader()
|
|
except PcscUnavailable as e:
|
|
_report_pcsc_unavailable(e.kind, e.cause)
|
|
return 2
|
|
sys.stderr.write('INIT: PC/SC reader detected: %s\n' % reader)
|
|
opts.pcsc_dev = 0
|
|
|
|
try:
|
|
kwargs = {}
|
|
if tracer is not None:
|
|
kwargs['apdu_tracer'] = tracer
|
|
t_phase = time.time()
|
|
sl = mod.init_reader(opts, **kwargs)
|
|
_tlog('init_reader: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
scc = SimCardCommands(sl)
|
|
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
|
|
scc._tp.proactive_handler = _DefaultProactiveHandler()
|
|
t_phase = time.time()
|
|
if opts.fast_init:
|
|
try:
|
|
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
|
|
except NoCardError:
|
|
# Normal cardless start: there was no card in the reader (the
|
|
# 3s wait timed out). The presence monitor auto-equips once a
|
|
# card appears; nothing to recover, no traceback.
|
|
sys.stderr.write('INIT: no card in the reader - server ready; insert a card or press Equip\n')
|
|
except Exception:
|
|
print("Warning: fast card initialization failed, falling back to pysim init:", file=sys.stderr)
|
|
traceback.print_exc()
|
|
try:
|
|
rs, card = mod.init_card(sl, opts.skip_card_init)
|
|
except NoCardError:
|
|
# The fallback retried the cardless wait; still a normal
|
|
# cardless start, not an initialization failure.
|
|
sys.stderr.write('INIT: no card in the reader - server ready; insert a card or press Equip\n')
|
|
else:
|
|
try:
|
|
sl.wait_for_card(3)
|
|
rs, card = mod.init_card(sl, opts.skip_card_init)
|
|
except NoCardError:
|
|
sys.stderr.write('INIT: no card in the reader - server ready; insert a card or press Equip\n')
|
|
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
if card is not None:
|
|
# Fast init builds the card on its own SimCardCommands instance;
|
|
# adopt it so server.scc carries the card's cla_byte/sel_ctrl (the
|
|
# placeholder created above is a different object left at the SIM
|
|
# defaults, which made VERIFY go out with CLA A0 on a UICC).
|
|
scc = card._scc
|
|
scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0'
|
|
except ReaderError as e:
|
|
sys.stderr.write('INIT: no PC/SC reader found: %s\n' % e)
|
|
sys.stderr.write(' Connect the reader (pcsc_scan lists them) and restart the server.\n')
|
|
return 2
|
|
except Exception as e:
|
|
kind = _pcsc_error_kind(e)
|
|
if kind:
|
|
_report_pcsc_unavailable(kind, e)
|
|
return 2
|
|
print("Warning: reader/card initialization failed:", file=sys.stderr)
|
|
traceback.print_exc()
|
|
ch = CardHandler(sl) if sl else None
|
|
t_phase = time.time()
|
|
try:
|
|
if card is not None:
|
|
app = mod.PysimApp(verbose=opts.verbose, card=card, rs=rs, sl=sl, ch=ch)
|
|
else:
|
|
# Cardless start: pySim logs 'Waiting for card...' (its own retry
|
|
# path) and pySim-shell prints 'pySim-shell not equipped!'; we
|
|
# report both cases with our own single line above. A PysimApp
|
|
# without a card would also retry the cardless wait, so install
|
|
# the filter before constructing it and drop the two internals.
|
|
saved_stdout = sys.stdout
|
|
sys.stdout = _LineFilter(saved_stdout, ('Waiting for card...', 'pySim-shell not equipped!'))
|
|
try:
|
|
app = mod.PysimApp(verbose=opts.verbose, card=None, rs=None, sl=sl, ch=ch)
|
|
finally:
|
|
sys.stdout = saved_stdout
|
|
except Exception:
|
|
print("Warning: PysimApp creation failed:", file=sys.stderr)
|
|
traceback.print_exc()
|
|
app = None
|
|
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
if app is not None and opts.fast_init:
|
|
fastinit.install(app)
|
|
iccid = None
|
|
netstate_files = None
|
|
arr_files = None
|
|
pbr_files = None
|
|
if scc and card is not None and hasattr(scc, '_tp'):
|
|
scc._tp.apdu_tracer = _LoggingApduTracer()
|
|
try:
|
|
_init_proactive_session()
|
|
# Read EF.ICCID before the TERMINAL PROFILE starts the CAT session
|
|
# (the PWA auto-selects the matching card preset from it).
|
|
iccid = _read_iccid(app)
|
|
if iccid:
|
|
sys.stderr.write('INIT: ICCID %s\n' % iccid)
|
|
# Network state monitor: read the network-related EFs in the
|
|
# same CAT-free window (skipped without a readable ICCID).
|
|
try:
|
|
netstate_files = _netstate_read(app)
|
|
except Exception:
|
|
netstate_files = None
|
|
# EF.ARR (the FCP '8B' references): read here too - a card
|
|
# present at server start never runs _apply_equipped_card,
|
|
# which owns this read on a later equip.
|
|
arr_files = _read_arr(app)
|
|
if arr_files:
|
|
sys.stderr.write('INIT: EF.ARR %s\n' % ', '.join(
|
|
'%s (%d rec)' % (a['fid'], a['num_records']) for a in arr_files))
|
|
# EF.PBR (the phonebook reference record): same reason - the
|
|
# startup init never runs _apply_equipped_card.
|
|
pbr_files = _read_pbr(app)
|
|
if pbr_files:
|
|
sys.stderr.write('INIT: EF.PBR %s\n' % ', '.join(
|
|
'%s (%d rec)' % (p['fid'], p['num_records']) for p in pbr_files))
|
|
t_phase = time.time()
|
|
sys.stderr.write('INIT: sending TERMINAL PROFILE %s (CLA=%s)\n' % (opts.terminal_profile, scc.cat_cla))
|
|
sm, el = _send_terminal_profile(scc, opts.terminal_profile)
|
|
sys.stderr.write('INIT: TP done, menu=%s events=%s\n' % ('yes' if sm else 'no', 'yes' if el else 'no'))
|
|
sim_menu = sm or sim_menu
|
|
event_list = el or event_list
|
|
for _ in range(3):
|
|
st_data, st_sw = _send_status(scc)
|
|
sys.stderr.write('INIT: drain STATUS -> %s\n' % st_sw)
|
|
if not st_sw.startswith('91'):
|
|
break
|
|
chain_sw = _handle_proactive_chain(scc, st_sw)
|
|
# the chain's trailing STATUS already proved the card idle;
|
|
# polling once more only costs an APDU (v3.38.1)
|
|
if chain_sw and not chain_sw.startswith('91'):
|
|
break
|
|
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
except Exception as e:
|
|
if _is_pcsc_error(e):
|
|
# The card (or the reader) went away during the startup init -
|
|
# a normal state, not a crash. Drop the half-initialized
|
|
# session: keeping the card object would leave the server
|
|
# reporting a phantom equipped card, and the auto-equip would
|
|
# then skip every later insertion (_CARD_CONNECTED is True).
|
|
# The presence monitor equips the next card (live 2026-10-21).
|
|
sys.stderr.write('INIT: card removed during initialization - server ready; '
|
|
'insert a card or press Equip\n')
|
|
iccid = None
|
|
netstate_files = None
|
|
arr_files = None
|
|
pbr_files = None
|
|
card = None
|
|
rs = None
|
|
scc = None
|
|
_clear_app_card_state(app)
|
|
else:
|
|
traceback.print_exc(file=sys.stderr)
|
|
if app is not None and tracer is not None:
|
|
if opts.apdu_trace:
|
|
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
|
|
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
|
|
# directly to fd 1 so it survives the app.stdout/StringIO redirection in the
|
|
# HTTP handlers and the INFO level suppression.
|
|
PySimLogger.setup(print_callback=_log_stdout)
|
|
PySimLogger.set_level(logging.DEBUG)
|
|
# PysimApp.__init__ and every `equip` wipe the transport apdu_tracer
|
|
# (_onchange_apdu_trace sets it to None). Re-attach our tracer(s) and make
|
|
# sure they stay attached across equip/re-equip.
|
|
def _reattach_tracer():
|
|
if app.card:
|
|
app.card._scc._tp.apdu_tracer = tracer
|
|
_reattach_tracer()
|
|
orig_onchange = app._onchange_apdu_trace
|
|
def _onchange_apdu_trace(param_name, old, new):
|
|
orig_onchange(param_name, old, new)
|
|
_reattach_tracer()
|
|
app._onchange_apdu_trace = _onchange_apdu_trace
|
|
server = _build_http_server(opts.http_host, opts.http_port, PysimHandler)
|
|
server.sl = sl
|
|
server.scc = scc
|
|
server.card = card
|
|
server.rs = rs
|
|
server.app = app
|
|
# PC/SC recovery: recreate the transport after a service failure (pcscd
|
|
# restart / reader re-enumeration); _ensure_transport() calls this.
|
|
def _new_transport():
|
|
return mod.init_reader(opts, **kwargs)
|
|
server.transport_factory = _new_transport
|
|
server.sms_oa = opts.sms_oa
|
|
server.sms_sc = opts.sms_sm_sc
|
|
server.log_requests = opts.log_requests
|
|
server.terminal_profile = opts.terminal_profile
|
|
server.cli_terminal_profile = opts.terminal_profile
|
|
server.web_dir = opts.web_dir
|
|
server.mcc_mnc_path = opts.mcc_mnc_list
|
|
# Card presets live server-side (the SCP80 counter is card state the server
|
|
# persists with the operation that consumed it); --card-presets overrides
|
|
# the default store in the user's home.
|
|
server.card_presets = presets.PresetStore(opts.card_presets)
|
|
sys.stderr.write('PRESETS: %s (%d presets)\n'
|
|
% (server.card_presets.path, len(server.card_presets.list())))
|
|
# Test scripts live server-side too (only the server can run them):
|
|
# --test-scripts overrides the default store, and the store validates a
|
|
# script with the same engine the runner uses, so a stored script can
|
|
# never fail at run start for a validation reason.
|
|
server.test_scripts = test_scripts.TestScriptStore(
|
|
opts.test_scripts,
|
|
validator=lambda raw: testscript.normalise_script(raw, _test_command_type))
|
|
sys.stderr.write('TESTSCRIPTS: %s (%d scripts)\n'
|
|
% (server.test_scripts.path, len(server.test_scripts.list())))
|
|
# Test suites group the scripts (v3.22.0): a script belongs to exactly one
|
|
# suite, so a pre-suite store's scripts are attached to an auto-created
|
|
# "Imported scripts" suite once (idempotent - no orphans, nothing to do).
|
|
server.test_suites = test_suites.TestSuiteStore(opts.test_suites)
|
|
sys.stderr.write('TESTSUITES: %s (%d suites)\n'
|
|
% (server.test_suites.path, len(server.test_suites.list())))
|
|
migrate_scripts_to_suites(server.test_scripts, server.test_suites)
|
|
server.sim_menu = sim_menu
|
|
server.event_list = event_list
|
|
server.menu_active = False
|
|
server.stk_pending = None
|
|
server.card_present = card is not None
|
|
server.card_session = 1 if card is not None else 0
|
|
# The startup init inlines the post-equip work (it never runs
|
|
# _apply_equipped_card): the batch worker's ready marker must be set
|
|
# here too (v3.28.0 audit).
|
|
server.card_ready_session = server.card_session
|
|
server.iccid = iccid
|
|
# EF.ARR cache (v3.24.0): read by the startup init above (a card present
|
|
# at server start); a later equip refreshes it in _apply_equipped_card.
|
|
server.arr = arr_files
|
|
# EF.PBR cache (TS 31.102 4.4.2.1): read by the startup init above; a
|
|
# later equip refreshes it in _apply_equipped_card.
|
|
server.pbr = pbr_files
|
|
# Network state monitor: install the state read during the startup init
|
|
# (right after the ICCID, before the TERMINAL PROFILE). No readable
|
|
# ICCID means the card is considered unusable - give up.
|
|
try:
|
|
_netstate_install(server, netstate_files if iccid else None)
|
|
except Exception as e:
|
|
server.net_state = None
|
|
sys.stderr.write('INIT: network state failed: %s\n' % e)
|
|
server.equipping = False
|
|
# Set server reference for polling timer and mark the card session state
|
|
import pysim_simple_server.server
|
|
pysim_simple_server.server._server_ref = server
|
|
pysim_simple_server.server._CARD_CONNECTED = card is not None
|
|
if opts.poll_interval is not None:
|
|
pysim_simple_server.server._set_poll_interval(opts.poll_interval)
|
|
# Background STATUS polling is on by default: a CAT terminal polls during
|
|
# idle (TS 102 221 14.6.2). --poll-interval 0 disables it, and a card
|
|
# that wants no polling suspends it with POLLING OFF (TS 102 223 6.4.14)
|
|
# until a new POLL INTERVAL.
|
|
if opts.poll_interval == 0:
|
|
pysim_simple_server.server._poll_disable()
|
|
else:
|
|
pysim_simple_server.server._poll_enable()
|
|
# Start presence monitoring only after the startup init: pyscard reports an
|
|
# already-present card as "added" on the first pass, and we must not
|
|
# auto-equip over a session we just initialized. If startup init failed,
|
|
# that event triggers auto-equip instead — the desired retry.
|
|
if sl is not None and getattr(sl, '_reader', None) is not None:
|
|
start_card_monitor(str(sl._reader))
|
|
print(_console_rule())
|
|
print(" pysim-simple-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port))
|
|
print(" Open http://%s:%s in your browser for the SIMple UI (served by this server)."
|
|
% (opts.http_host, opts.http_port))
|
|
print(_console_rule())
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down...")
|
|
server.shutdown()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main()) |