diff --git a/README.md b/README.md index a8a0219..c2427c8 100644 --- a/README.md +++ b/README.md @@ -597,7 +597,7 @@ The **Listener** starts/stops the target the card's BIP channel is redirected to - **Pass-through (card destination)** — no listener and no target: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (`Other address` + transport port, TCP client remote only; no spec default port, so an incomplete request fails the channel). TLS is terminated by that platform, so the server's network is used (lab only). - **Capture (dump)** — accepts the card's TCP channel and logs whatever it sends (e.g. the TLS ClientHello) without answering. -The **Options** block (applied at **Start**, remembered in the browser) exposes the HTTP framing (chunked body / chunk size, 0 = one TLS record / keep-alive / Connection header / compact headers / Next-URI), the script framing (indefinite `AE 80 …` or definite `AA` Command Scripting template, comprehension-required tags, targeted application) and link events (all modes, TS 102 223 §7.5.11). TLS has no settings: the listener offers TLS 1.0–1.2 and all PSK suites and lets the card negotiate; the log and state line report the version/cipher actually used, and a TLS/cipher handshake failure is logged as `tls-handshake-failed`. +The **Options** block (collapsed by default; a `custom` marker appears when anything differs from the reference defaults; applied at **Start**, remembered in the browser) exposes the HTTP framing (Chunked body / chunk size, 0 = one TLS record / Connection header / compact headers, legal per RFC 7230 §3.2 / Next-URI checkbox + template, unchecked = one-shot), the script framing (indefinite `AE 80 …` or definite `AA` Command Scripting template, comprehension-required tags, optional `X-Admin-Targeted-Application` with its own checkbox) and **Show link events** (all modes, TS 102 223 §7.5.11). The connection stays open between POSTs and closes cleanly at the 204. TLS has no settings: the listener offers TLS 1.0–1.2 and all PSK suites and lets the card negotiate; the log and state line report the version/cipher actually used, and a TLS/cipher handshake failure is logged as `tls-handshake-failed`. **Script** selects the command list served over the session: **None** (leave the server's configured script) or one of the scripts created in the **Scripts** pill; **Restart script** re-queues the selected script with `force`, starting over from the first APDU. diff --git a/README_RUS.md b/README_RUS.md index 21a3bc5..729e35b 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -567,7 +567,7 @@ Delivery PoR (SPI2 `01`) проще — карта возвращает PoR на - **Проброс (адрес карты)** — без слушателя и без цели: терминал подключает каждый BIP-канал к адресу, который карта запрашивает в OPEN CHANNEL (`Other address` + порт transport level, только TCP-клиент, удалённое соединение; порт по умолчанию спецификациями не задан, поэтому неполный запрос отклоняет канал). TLS завершается на той платформе, и используется сеть сервера (только для лаборатории). - **Capture (dump)** — принимает TCP-канал карты и записывает всё, что она отправляет (например, TLS ClientHello), не отвечая. -Блок **Настройки** (применяются при **Start**, сохраняются в браузере) открывает HTTP-фрейминг (chunked-тело / размер чанка, 0 = одна TLS-запись / keep-alive / заголовок Connection / компактные заголовки / Next-URI), фрейминг скрипта (неопределённая `AE 80 …` или определённая `AA` длина Command Scripting template, теги с обязательным пониманием, целевое приложение) и события канала (во всех режимах, TS 102 223 §7.5.11). У TLS нет настроек: слушатель предлагает TLS 1.0–1.2 и все PSK-наборы, а карта выбирает; журнал и строка состояния показывают фактически использованные версию и набор, а сбой рукопожатия по причине TLS/набора пишется как `tls-handshake-failed`. +Блок **Настройки** (свёрнут по умолчанию, метка «изменены» при отличиях; применяются при **Start**, сохраняются в браузере) открывает HTTP-фрейминг (Chunked body / размер чанка, 0 = одна TLS-запись / заголовок Connection / компактные заголовки, допустимо по RFC 7230 §3.2 / Next-URI: флажок + шаблон, снято — одноразовый режим), фрейминг скрипта (неопределённая `AE 80 …` или определённая `AA` длина Command Scripting template, теги comprehension-required, необязательный `X-Admin-Targeted-Application` со своим флажком) и **Показывать события канала** (во всех режимах, TS 102 223 §7.5.11). Соединение удерживается между POST-запросами и аккуратно закрывается на 204. У TLS нет настроек: слушатель предлагает TLS 1.0–1.2 и все PSK-наборы, а карта выбирает; журнал и строка состояния показывают фактически использованные версию и набор, а сбой рукопожатия по причине TLS/набора пишется как `tls-handshake-failed`. **Script** выбирает список команд, отдаваемый в сессии: **None** (оставить настроенный на сервере скрипт) или один из скриптов, созданных на подвкладке **Scripts**; **Restart script** заново ставит выбранный скрипт в очередь с `force`, начиная с первого APDU. diff --git a/docs/api.md b/docs/api.md index 4197e9b..1639085 100644 --- a/docs/api.md +++ b/docs/api.md @@ -672,16 +672,20 @@ record, as in the decrypted reference session; a positive value writes the head and each body piece as its own record). Both are echoed by `GET /api/scp81/status`. -`keep_alive` (default `true`, matching the reference session: the card sends -all its POSTs on one connection until the 204) ends the TLS connection after -each response -(after the card drained the BIP buffer, with `close_notify`, so the card -processes the script and opens a new connection for its next POST); -`compact_headers` (default `false`) drops the space after each header colon, -`conn_header` (default `'none'` = omit the header) declares the connection -fate, `tls_version` (default `'auto'` — accept TLS 1.0-1.2 and let OpenSSL -pick the highest the card offers; `'1.0'`/`'1.1'`/`'1.2'` pin a version for -debugging) selects the protocol window, `cipher` pins one suite, `next_uri` +The connection stays open between POSTs for the whole dialog (the card is the +HTTP client and may reuse it or dial a new one at will — GP Am. B 4.3.1 leaves +connection management to the SD); only the 204 ends the session, and the server +then shuts the TLS session down cleanly (`close_notify` while the response is +still buffered, then FIN). There is no option to close after every response. + +`compact_headers` (default `false`) omits the optional space after each header +colon — legal per RFC 7230 3.2 (OWS), though a single SP is preferred, and it +saves one byte per header (useful to keep a response within one card-sized TLS +record); `conn_header` (default `'none'` = omit the header; `'keep-alive'` adds +it) declares the connection fate, `tls_version` (default `'auto'` — accept TLS +1.0-1.2 and let OpenSSL pick the highest the card offers; `'1.0'`/`'1.1'`/`'1.2'` +pin a version for debugging) selects the protocol window, `cipher` pins one +suite, `next_uri` overrides the per-command `X-Admin-Next-URI` (`%d` = command id; empty string omits the header), `link_events` (default `true`, all modes) controls the automatic Channel status events, `answer_delay` waits before answering a @@ -702,8 +706,9 @@ reasons (no shared cipher, unsupported protocol version, card alert) is logged as `tls-handshake-failed` with the OpenSSL reason, separately from post-handshake record errors (`tls-error`). -The PWA's Listener **Options** block exposes `chunked`, `chunk_size`, -`keep_alive`, `conn_header`, `compact_headers`, `next_uri`, -`script_template`, `cr_tag`, `targeted_app` and `link_events` -(applied at Start, persisted in `localStorage`); `tls_version`, `cipher`, -`keylog` and `answer_delay` stay API-only. +The PWA's Listener **Options** block (collapsed by default; a `custom` marker +appears when anything differs from the reference defaults) exposes `chunked`, +`chunk_size`, `conn_header`, `compact_headers`, `next_uri` (checkbox + template), +`script_template`, `cr_tag`, `targeted_app` (checkbox + `//aid/...` field) and +`link_events` (applied at Start, persisted in `localStorage`); `tls_version`, +`cipher`, `keylog` and `answer_delay` stay API-only. diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 5b7c022..9ab3840 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -327,7 +327,7 @@
  • Проброс (адрес карты) — без слушателя и без цели: терминал подключает каждый BIP-канал к адресу, который карта запрашивает в OPEN CHANNEL (Other address + порт Transport level, только TCP-клиент, удалённое соединение; спецификации не задают порт по умолчанию, поэтому неполный запрос отклоняет канал). TLS завершается на той платформе, и используется сеть сервера (только для лаборатории).
  • Захват (dump) — принимает TCP-канал карты и записывает всё, что она отправляет (например, TLS ClientHello), не отвечая.
  • -

    Блок Настройки (применяются при Start, сохраняются в браузере) открывает HTTP-фрейминг — chunked-тело / размер чанка (0 = весь ответ в одной TLS-записи), keep-alive, заголовок Connection, компактные заголовки и Next-URI (снято — заголовок не отправляется) — и фрейминг скрипта (неопределённая длина AE 80 … / определённая AA в Command Scripting template, теги с обязательным пониманием, целевое приложение). События канала (ENVELOPE Channel status, TS 102 223 §7.5.11) действуют во всех режимах. У TLS нет настроек: слушатель предлагает TLS 1.0–1.2 и все PSK-наборы, а карта выбирает; строка состояния и TLS-журнал показывают фактически использованные версию и набор, а сбой рукопожатия по причине TLS/набора пишется как tls-handshake-failed.

    +

    Блок Настройки (по умолчанию свёрнут; при отличии от эталонных значений показывается метка изменены; применяются при Start, сохраняются в браузере) открывает HTTP-фрейминг — Chunked body / размер чанка (0 = весь ответ в одной TLS-записи), заголовок Connection, компактные заголовки (без необязательного пробела после :, допустимо по RFC 7230 §3.2) и Next-URI (флажок + шаблон; снято — одноразовый режим: карта выполнит команду, не вернёт строку ответа и закроет сессию) — и фрейминг скрипта (неопределённая длина AE 80 … / определённая AA в Command Scripting template, теги comprehension-required и необязательный X-Admin-Targeted-Application со своим флажком). Показывать события канала (ENVELOPE Channel status, TS 102 223 §7.5.11) действует во всех режимах. Соединение удерживается между POST-запросами и аккуратно закрывается на 204. У TLS нет настроек: слушатель предлагает TLS 1.0–1.2 и все PSK-наборы, а карта выбирает; строка состояния и TLS-журнал показывают фактически использованные версию и набор, а сбой рукопожатия по причине TLS/набора пишется как tls-handshake-failed.

    Сценарий выбирает список команд, отдаваемый в сессии — Нет (оставить настроенный на сервере скрипт) или один из скриптов подвкладки «Скрипты»; выбранный список передаётся серверу при старте слушателя.

    Строка состояния показывает слушатель, согласованный идентификатор и активные каналы (байты in/out). Панель Script results (R-APDUs) перечисляет каждый отданный C-APDU с его R-APDU/SW и прогрессом done/total; журнал HTTP OTA log фиксирует OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA и каждый шаг TLS/HTTP/скрипта (tls-handshake, tls-request, script-send, script-rapdu, script-page, script-done, data-available, peer-close), кнопка Clear очищает его.

    5.2 Скрипты

    diff --git a/frontend/help.html b/frontend/help.html index a39d90b..e548d5f 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -327,7 +327,7 @@
  • Pass-through (card destination) — no listener and no target: the terminal connects each BIP channel to the destination the card requests in OPEN CHANNEL (Other address + Transport level port, TCP client remote only; the specs define no default port, so an incomplete request fails the channel). TLS is terminated by that platform, and the server's network is used (lab only).
  • Capture (dump) — accepts the card's TCP channel and logs whatever it sends (e.g. the TLS ClientHello) without answering.
  • -

    The Options block (applied at Start, remembered in the browser) exposes the HTTP framing — chunked body / chunk size (0 = the whole response in one TLS record), keep-alive, the Connection header, compact headers and the Next-URI (unchecked = omit the header) — and the script framing (indefinite AE 80 … / definite AA Command Scripting template, comprehension-required tags, targeted application). Link events (Channel status ENVELOPEs, TS 102 223 §7.5.11) apply to every mode. TLS itself has no settings: the listener offers TLS 1.0–1.2 and all PSK suites and lets the card negotiate; the state line and the TLS log show the version/cipher actually used, and a handshake that fails for a TLS/cipher reason is logged as tls-handshake-failed.

    +

    The Options block (collapsed by default; a custom marker appears when anything differs from the reference defaults; applied at Start, remembered in the browser) exposes the HTTP framing — Chunked body / chunk size (0 = the whole response in one TLS record), the Connection header, compact headers (omitting the optional space after :, legal per RFC 7230 §3.2) and the Next-URI (checkbox + template; unchecked = one-shot: the card executes the command, returns no response string and closes the session) — and the script framing (indefinite AE 80 … / definite AA Command Scripting template, comprehension-required tags, and the optional X-Admin-Targeted-Application with its own checkbox). Show link events (Channel status ENVELOPEs, TS 102 223 §7.5.11) applies to every mode. The connection is kept open between POSTs and closed cleanly at the 204. TLS itself has no settings: the listener offers TLS 1.0–1.2 and all PSK suites and lets the card negotiate; the state line and the TLS log show the version/cipher actually used, and a handshake that fails for a TLS/cipher reason is logged as tls-handshake-failed.

    Script selects the command list served over the session — None (leave the server's configured script) or one of the scripts from the Scripts pill; the chosen list is sent to the server when the listener starts.

    The state line shows the listener, the negotiated identity and live channels (bytes in/out). Script results (R-APDUs) lists each served C-APDU with its R-APDU/SW and a done/total progress; the HTTP OTA log records OPEN/CLOSE CHANNEL, SEND/RECEIVE DATA and every TLS/HTTP/script step (tls-handshake, tls-request, script-send, script-rapdu, script-page, script-done, data-available, peer-close), with Clear.

    5.2 Scripts

    diff --git a/frontend/index.html b/frontend/index.html index 1b2cd41..fc579cb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
    -

    OTAMan SIM OTA with a Human Face v2.2.14

    +

    OTAMan SIM OTA with a Human Face v2.2.15

    @@ -995,44 +995,49 @@
    -
    -
    - Options (applied at Start) - -
    -
    -
    HTTP framing
    - -
    - - +
    + + Options (applied at Start) + + +
    +
    +
    - -
    - - +
    +
    HTTP framing
    + +
    + + +
    +
    + + +
    + + + +
    Unchecked = one-shot: per GP Am. B §4.4.2 the card executes the command, returns no response string and closes the session.
    - - - -
    -
    -
    Script framing
    -
    - - +
    +
    Script framing
    +
    + + +
    + +
    - +
    - -
    +
    Script results (R-APDUs) @@ -7810,58 +7815,102 @@ let _scp81LastStatus = null; // ===== SCP81 listener options (HTTP framing / script framing / link events) ===== // The API takes these at listener start; they are kept in localStorage so a -// test setup survives reloads. TLS itself has no settings: the listener -// accepts everything it can and reports the negotiated version/cipher. +// test setup survives reloads. The block is collapsed by default and shows a +// 'custom' marker when anything differs from the reference defaults. TLS +// itself has no settings: the listener accepts everything it can and reports +// the negotiated version/cipher. const SCP81_OPT_DEFAULTS = { - chunked: true, chunkSize: 0, keepAlive: true, connHeader: 'none', - compact: false, nextUri: '/api/scp81?req=%d', linkEvents: true, - scriptTemplate: 'indefinite', crTag: false, targetedApp: '' + chunked: true, chunkSize: 0, connHeader: 'none', compact: false, + nextUriOn: true, nextUri: '/api/scp81?req=%d', linkEvents: true, + scriptTemplate: 'indefinite', crTag: false, + targetedAppOn: false, targetedApp: '' }; +function scp81OptionsState() { + const val = id => { const el = document.getElementById(id); return el ? el.value : ''; }; + const on = id => { const el = document.getElementById(id); return !!(el && el.checked); }; + return { + chunked: on('opt-chunked'), + chunkSize: Math.max(0, parseInt(val('opt-chunk-size'), 10) || 0), + connHeader: val('opt-conn-header') || 'none', + compact: on('opt-compact'), + nextUriOn: on('opt-next-uri'), + nextUri: val('opt-next-uri-value').trim(), + linkEvents: on('opt-link-events'), + scriptTemplate: val('opt-script-template') === 'definite' ? 'definite' : 'indefinite', + crTag: on('opt-cr-tag'), + targetedAppOn: on('opt-targeted-app-on'), + targetedApp: val('opt-targeted-app').trim() + }; +} + +function scp81OptionsFromForm() { + const s = scp81OptionsState(); + return { + chunked: s.chunked, + chunk_size: s.chunkSize, + conn_header: s.connHeader, + compact_headers: s.compact, + next_uri: s.nextUriOn ? (s.nextUri || SCP81_OPT_DEFAULTS.nextUri) : '', + link_events: s.linkEvents, + script_template: s.scriptTemplate, + cr_tag: s.crTag, + targeted_app: s.targetedAppOn ? s.targetedApp : '' + }; +} + +function scp81OptionsSyncDisabled() { + const on = id => { const el = document.getElementById(id); return !!(el && el.checked); }; + const sync = (id, enabled) => { + const el = document.getElementById(id); + if (!el) return; + el.disabled = !enabled; + el.classList.toggle('opacity-40', !enabled); + }; + sync('opt-next-uri-value', on('opt-next-uri')); + sync('opt-targeted-app', on('opt-targeted-app-on')); +} + +function scp81OptionsBadge() { + const el = document.getElementById('scp81-opts-badge'); + if (!el) return; + const s = scp81OptionsState(); + const d = SCP81_OPT_DEFAULTS; + const same = s.chunked === d.chunked && s.chunkSize === d.chunkSize && + s.connHeader === d.connHeader && s.compact === d.compact && + s.nextUriOn === d.nextUriOn && (s.nextUri || d.nextUri) === d.nextUri && + s.linkEvents === d.linkEvents && s.scriptTemplate === d.scriptTemplate && + s.crTag === d.crTag && s.targetedAppOn === d.targetedAppOn && + s.targetedApp === d.targetedApp; + el.textContent = same ? '' : t('custom'); +} + function scp81OptionsLoad() { let saved = {}; try { saved = JSON.parse(localStorage.getItem('otaman_scp81_opts') || '{}') || {}; } catch (e) { saved = {}; } const opt = Object.assign({}, SCP81_OPT_DEFAULTS, saved); + if (saved.nextUriOn === undefined) opt.nextUriOn = (opt.nextUri || '') !== ''; const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; }; const chk = (id, v) => { const el = document.getElementById(id); if (el) el.checked = !!v; }; chk('opt-chunked', opt.chunked); set('opt-chunk-size', opt.chunkSize); - chk('opt-keep-alive', opt.keepAlive); - set('opt-conn-header', opt.connHeader); + set('opt-conn-header', opt.connHeader === 'keep-alive' ? 'keep-alive' : 'none'); chk('opt-compact', opt.compact); - chk('opt-next-uri', opt.nextUri !== ''); + chk('opt-next-uri', opt.nextUriOn); set('opt-next-uri-value', opt.nextUri || SCP81_OPT_DEFAULTS.nextUri); chk('opt-link-events', opt.linkEvents); set('opt-script-template', opt.scriptTemplate === 'definite' ? 'definite' : 'indefinite'); chk('opt-cr-tag', opt.crTag); + chk('opt-targeted-app-on', opt.targetedAppOn); set('opt-targeted-app', opt.targetedApp); -} - -function scp81OptionsFromForm() { - const val = id => { const el = document.getElementById(id); return el ? el.value : ''; }; - const on = id => { const el = document.getElementById(id); return !!(el && el.checked); }; - return { - chunked: on('opt-chunked'), - chunk_size: Math.max(0, parseInt(val('opt-chunk-size'), 10) || 0), - keep_alive: on('opt-keep-alive'), - conn_header: val('opt-conn-header') || 'none', - compact_headers: on('opt-compact'), - next_uri: on('opt-next-uri') ? (val('opt-next-uri-value').trim() || SCP81_OPT_DEFAULTS.nextUri) : '', - link_events: on('opt-link-events'), - script_template: val('opt-script-template') === 'definite' ? 'definite' : 'indefinite', - cr_tag: on('opt-cr-tag'), - targeted_app: val('opt-targeted-app').trim() - }; + scp81OptionsSyncDisabled(); + scp81OptionsBadge(); } function scp81OptionsPersist() { - const o = scp81OptionsFromForm(); - localStorage.setItem('otaman_scp81_opts', JSON.stringify({ - chunked: o.chunked, chunkSize: o.chunk_size, keepAlive: o.keep_alive, - connHeader: o.conn_header, compact: o.compact_headers, nextUri: o.next_uri, - linkEvents: o.link_events, scriptTemplate: o.script_template, - crTag: o.cr_tag, targetedApp: o.targeted_app - })); + localStorage.setItem('otaman_scp81_opts', JSON.stringify(scp81OptionsState())); + scp81OptionsSyncDisabled(); + scp81OptionsBadge(); } function scp81OptionsReset() { @@ -7869,6 +7918,16 @@ function scp81OptionsReset() { scp81OptionsLoad(); } +function scp81NextUriToggle() { + scp81OptionsSyncDisabled(); + scp81OptionsPersist(); +} + +function scp81TargetedAppToggle() { + scp81OptionsSyncDisabled(); + scp81OptionsPersist(); +} + function scp81ModeChanged() { const mode = document.getElementById('scp81-mode').value; const row = document.getElementById('scp81-script-row'); @@ -8242,7 +8301,6 @@ async function scp81StatusRefresh() { s += ' | ' + (l.chunked ? ('chunked' + (l.chunk_size ? ' ' + l.chunk_size : '')) : 'content-length'); - if (l.keep_alive) s += ' | keep-alive'; } const ch = (bip.channels || []).map(c => 'ch' + c.id + (c.target ? ' → ' + c.target : '') + ' in:' + c.bytes_in + ' out:' + c.bytes_out).join(', '); if (ch) s += ' | ' + ch; @@ -8289,7 +8347,6 @@ async function scp81Start() { if (mode === 'tls') { body.chunked = opts.chunked; body.chunk_size = opts.chunk_size; - body.keep_alive = opts.keep_alive; body.conn_header = opts.conn_header; body.compact_headers = opts.compact_headers; body.next_uri = opts.next_uri; @@ -11579,18 +11636,20 @@ const LANG_RU = { 'Options (applied at Start)': 'Настройки (применяются при запуске)', 'Reset to defaults': 'Сбросить к умолчаниям', 'HTTP framing': 'HTTP-фрейминг', - 'Chunked body (Transfer-Encoding: chunked)': 'Chunked-тело (Transfer-Encoding: chunked)', + 'Chunked body (Transfer-Encoding: chunked)': 'Chunked body (Transfer-Encoding: chunked)', 'Chunk size (bytes, 0 = one TLS record)': 'Размер чанка (байт, 0 = одна TLS-запись)', - 'Keep-alive (one connection until the session ends)': 'Keep-alive (одно соединение до конца сессии)', 'Connection header': 'Заголовок Connection', 'omit (implicit keep-alive)': 'не отправлять (неявный keep-alive)', - "Compact headers (no space after ':')": 'Компактные заголовки (без пробела после «:»)', - 'Next-URI (unchecked = omit the header)': 'Next-URI (снято — заголовок не отправляется)', + "Compact headers (omit the optional space after ':')": 'Компактные заголовки (без необязательного пробела после «:»)', + 'Send X-Admin-Next-URI (required to continue the session)': 'Отправлять X-Admin-Next-URI (нужен для продолжения сессии)', + 'Unchecked = one-shot: per GP Am. B §4.4.2 the card executes the command, returns no response string and closes the session.': 'Снято — одноразовый режим: по GP Am. B §4.4.2 карта выполнит команду, не вернёт строку ответа и закроет сессию.', + 'Send X-Admin-Targeted-Application': 'Отправлять X-Admin-Targeted-Application', + 'custom': 'изменены', 'Script framing': 'Фрейминг скрипта', 'Indefinite (AE 80 … 00 00)': 'Неопределённая длина (AE 80 … 00 00)', 'Definite (AA)': 'Определённая длина (AA)', - 'Comprehension-required tags': 'Теги с обязательным пониманием (CR)', - 'Link events (Channel status ENVELOPEs, TS 102 223 7.5.11)': 'События канала (ENVELOPE Channel status, TS 102 223 7.5.11)', + 'Comprehension-required tags': 'Теги comprehension-required', + 'Show link events (Channel status ENVELOPEs, TS 102 223 7.5.11)': 'Показывать события канала (ENVELOPE Channel status, TS 102 223 7.5.11)', 'ADM verified': 'ADM подтверждён', 'ADM not verified': 'ADM не подтверждён', 'TERMINAL PROFILE': 'TERMINAL PROFILE', diff --git a/frontend/sw.js b/frontend/sw.js index 4fb9e9a..61e1bc1 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v180'; +const CACHE = 'otaman-v181'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/scp81_options.test.js b/frontend/tests/scp81_options.test.js index a6e330b..93b732a 100644 --- a/frontend/tests/scp81_options.test.js +++ b/frontend/tests/scp81_options.test.js @@ -22,16 +22,18 @@ function extractFunc(src, name) { } let code = html.match(/const SCP81_OPT_DEFAULTS = \{[\s\S]*?\n\};/)[0].replace('const ', 'var ') + '\n'; -for (const fn of ['scp81OptionsLoad', 'scp81OptionsFromForm', 'scp81OptionsPersist', 'scp81OptionsReset']) { +for (const fn of ['scp81OptionsState', 'scp81OptionsFromForm', 'scp81OptionsSyncDisabled', + 'scp81OptionsBadge', 'scp81OptionsLoad', 'scp81OptionsPersist', 'scp81OptionsReset', + 'scp81NextUriToggle', 'scp81TargetedAppToggle']) { code += extractFunc(html, fn) + '\n'; } +code += 'globalThis.t = s => s;\n'; eval(code); function setup(opts) { const spec = Object.assign({ 'opt-chunked': true, 'opt-chunk-size': 0, - 'opt-keep-alive': true, 'opt-conn-header': 'none', 'opt-compact': false, 'opt-next-uri': true, @@ -39,11 +41,20 @@ function setup(opts) { 'opt-link-events': true, 'opt-script-template': 'indefinite', 'opt-cr-tag': false, - 'opt-targeted-app': '' + 'opt-targeted-app-on': false, + 'opt-targeted-app': '', + 'scp81-opts-badge': '' }, opts || {}); const els = {}; for (const [id, v] of Object.entries(spec)) { - els[id] = typeof v === 'boolean' ? { checked: v, value: '' } : { checked: true, value: String(v) }; + const checkbox = typeof v === 'boolean'; + els[id] = { + checked: checkbox ? v : true, + value: checkbox ? '' : String(v), + disabled: false, + textContent: '', + classList: { toggle() {} }, + }; } globalThis.document = { getElementById: id => els[id] || null }; return els; @@ -65,7 +76,6 @@ test('scp81OptionsFromForm maps the reference defaults', () => { assert.deepStrictEqual(scp81OptionsFromForm(), { chunked: true, chunk_size: 0, - keep_alive: true, conn_header: 'none', compact_headers: false, next_uri: '/api/scp81?req=%d', @@ -80,20 +90,19 @@ test('scp81OptionsFromForm reflects a changed setup', () => { setup({ 'opt-chunked': false, 'opt-chunk-size': '100', - 'opt-keep-alive': false, - 'opt-conn-header': 'close', + 'opt-conn-header': 'keep-alive', 'opt-compact': true, 'opt-next-uri-value': '/adminserver?apdu_id=%d', 'opt-link-events': false, 'opt-script-template': 'definite', 'opt-cr-tag': true, + 'opt-targeted-app-on': true, 'opt-targeted-app': ' //aid/A000000151000000 ' }); assert.deepStrictEqual(scp81OptionsFromForm(), { chunked: false, chunk_size: 100, - keep_alive: false, - conn_header: 'close', + conn_header: 'keep-alive', compact_headers: true, next_uri: '/adminserver?apdu_id=%d', link_events: false, @@ -110,51 +119,95 @@ test('unchecked Next-URI omits the header; empty text falls back to the template assert.strictEqual(scp81OptionsFromForm().next_uri, '/api/scp81?req=%d'); }); +test('X-Admin-Targeted-Application is only sent when its checkbox is ticked', () => { + // text left in the box with the checkbox off must never leak out + setup({ 'opt-targeted-app-on': false, 'opt-targeted-app': '//aid/A000000151000000' }); + assert.strictEqual(scp81OptionsFromForm().targeted_app, ''); + setup({ 'opt-targeted-app-on': true, 'opt-targeted-app': ' //aid/A000000151000000 ' }); + assert.strictEqual(scp81OptionsFromForm().targeted_app, '//aid/A000000151000000'); +}); + +test('the dependent fields are disabled while their checkbox is off', () => { + const els = setup({ 'opt-next-uri': false, 'opt-targeted-app-on': false }); + scp81OptionsSyncDisabled(); + assert.strictEqual(els['opt-next-uri-value'].disabled, true); + assert.strictEqual(els['opt-targeted-app'].disabled, true); + els['opt-next-uri'].checked = true; + els['opt-targeted-app-on'].checked = true; + scp81OptionsSyncDisabled(); + assert.strictEqual(els['opt-next-uri-value'].disabled, false); + assert.strictEqual(els['opt-targeted-app'].disabled, false); +}); + +test('the custom badge flags anything that differs from the defaults', () => { + const els = setup(); + scp81OptionsBadge(); + assert.strictEqual(els['scp81-opts-badge'].textContent, ''); + setup({ 'opt-compact': true, 'scp81-opts-badge': '' }); + scp81OptionsBadge(); + assert.strictEqual(globalThis.document.getElementById('scp81-opts-badge').textContent, 'custom'); + // an empty Next-URI text is not a change (the default template applies) + setup({ 'opt-next-uri-value': '', 'scp81-opts-badge': '' }); + scp81OptionsBadge(); + assert.strictEqual(globalThis.document.getElementById('scp81-opts-badge').textContent, ''); +}); + test('load/persist/reset round-trip through localStorage', () => { const store = fakeStorage(); setup({ 'opt-chunked': false, 'opt-chunk-size': 100, - 'opt-keep-alive': false, - 'opt-conn-header': 'close', + 'opt-conn-header': 'keep-alive', 'opt-compact': true, 'opt-next-uri': false, 'opt-link-events': false, 'opt-script-template': 'definite', 'opt-cr-tag': true, + 'opt-targeted-app-on': true, 'opt-targeted-app': '//aid/A000000151000000' }); scp81OptionsPersist(); assert.ok(store['otaman_scp81_opts'].includes('"chunkSize":100')); + assert.ok(store['otaman_scp81_opts'].includes('"targetedAppOn":true')); + assert.ok(!store['otaman_scp81_opts'].includes('keepAlive')); // a reload restores the saved setup setup({}); scp81OptionsLoad(); - assert.strictEqual(globalThis.document.getElementById('opt-chunked').checked, false); - assert.strictEqual(globalThis.document.getElementById('opt-chunk-size').value, 100); - assert.strictEqual(globalThis.document.getElementById('opt-conn-header').value, 'close'); - assert.strictEqual(globalThis.document.getElementById('opt-next-uri').checked, false); - assert.strictEqual(globalThis.document.getElementById('opt-script-template').value, 'definite'); - assert.strictEqual(globalThis.document.getElementById('opt-cr-tag').checked, true); - assert.strictEqual(globalThis.document.getElementById('opt-targeted-app').value, '//aid/A000000151000000'); + const get = id => globalThis.document.getElementById(id); + assert.strictEqual(get('opt-chunked').checked, false); + assert.strictEqual(get('opt-chunk-size').value, 100); + assert.strictEqual(get('opt-conn-header').value, 'keep-alive'); + assert.strictEqual(get('opt-next-uri').checked, false); + assert.strictEqual(get('opt-script-template').value, 'definite'); + assert.strictEqual(get('opt-cr-tag').checked, true); + assert.strictEqual(get('opt-targeted-app-on').checked, true); + assert.strictEqual(get('opt-targeted-app').value, '//aid/A000000151000000'); // reset clears the saved entry and restores the defaults scp81OptionsReset(); assert.ok(!('otaman_scp81_opts' in store)); scp81OptionsLoad(); - assert.strictEqual(globalThis.document.getElementById('opt-chunked').checked, true); - assert.strictEqual(globalThis.document.getElementById('opt-next-uri').checked, true); - assert.strictEqual(globalThis.document.getElementById('opt-conn-header').value, 'none'); + assert.strictEqual(get('opt-chunked').checked, true); + assert.strictEqual(get('opt-next-uri').checked, true); + assert.strictEqual(get('opt-conn-header').value, 'none'); + assert.strictEqual(get('opt-targeted-app-on').checked, false); }); test('the Listener UI wires the framing options into Start', () => { - for (const id of ['scp81-opts-http', 'scp81-opts-script', 'opt-chunked', - 'opt-chunk-size', 'opt-keep-alive', 'opt-conn-header', 'opt-compact', + for (const id of ['scp81-opts', 'scp81-opts-badge', 'scp81-opts-http', 'scp81-opts-script', + 'opt-chunked', 'opt-chunk-size', 'opt-conn-header', 'opt-compact', 'opt-next-uri', 'opt-next-uri-value', 'opt-script-template', 'opt-cr-tag', - 'opt-targeted-app', 'opt-link-events']) { + 'opt-targeted-app-on', 'opt-targeted-app', 'opt-link-events']) { assert.ok(html.includes('id="' + id + '"'), id); } - assert.ok(html.includes('body.link_events = opts.link_events;')); + // collapsed by default: a
    without the open attribute + assert.match(html, /
    /); + assert.ok(!/
    ]*\sopen/.test(html)); + // the removed switch must not come back + assert.ok(!html.includes('opt-keep-alive')); + // the options travel in the start body assert.ok(html.includes('body.chunk_size = opts.chunk_size;')); assert.ok(html.includes('body.next_uri = opts.next_uri;')); + assert.ok(html.includes('body.targeted_app = opts.targeted_app || null;')); assert.ok(html.includes('body.script_template = opts.script_template;')); assert.ok(html.includes("httpOpts.classList.toggle('hidden', mode !== 'tls')")); assert.ok(html.includes("scriptOpts.classList.toggle('hidden', mode !== 'tls')")); diff --git a/pyproject.toml b/pyproject.toml index f8fdd37..3779a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-otaman-server" -version = "2.2.14" +version = "2.2.15" description = "HTTP REST server wrapping pysim for the OTAMan PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_otaman_server/scp81.py b/pysim_otaman_server/scp81.py index 7aaa999..c031517 100644 --- a/pysim_otaman_server/scp81.py +++ b/pysim_otaman_server/scp81.py @@ -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() diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 73a864a..63f8cb0 100644 --- a/pysim_otaman_server/server.py +++ b/pysim_otaman_server/server.py @@ -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), diff --git a/tests/test_scp81.py b/tests/test_scp81.py index 426a537..857bece 100644 --- a/tests/test_scp81.py +++ b/tests/test_scp81.py @@ -189,8 +189,7 @@ class PskTlsServerTest(unittest.TestCase): server._SCP81_SCRIPT_NEXT = 0 server._SCP81_SCRIPT_RESULTS = [] srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, - responder=server._scp81_script_responder, - keep_alive=True) + responder=server._scp81_script_responder) try: tls = self._connect(srv) tls.sendall(b'POST /api/scp81 HTTP/1.1\r\nHost: 127.0.0.1\r\n' @@ -216,51 +215,46 @@ class PskTlsServerTest(unittest.TestCase): server._SCP81_SCRIPT_RESULTS = [] srv.stop() - def test_response_closes_connection_without_keep_alive(self): - # Default (keep_alive=False): the card's HTTP client seems to delimit - # the response at connection close, so the server closes after each - # response and the card starts a fresh session for its next POST. + def test_200_keeps_the_connection_for_the_next_post(self): + # The card is the HTTP client and may reuse the connection for its + # next POST (GP Am. B 4.3.1: connection management is the SD's job); + # the server never closes between requests. def responder(method, target, headers, body): return 200, {'X-Admin-Protocol': scp81.GP_PROTOCOL}, b'\x80\x01\x00' srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, responder=responder) try: tls = self._connect(srv) - tls.sendall(b'POST /api/scp81 HTTP/1.1\r\n\r\n') + tls.sendall(b'POST /api/scp81?req=1 HTTP/1.1\r\n\r\n') + reply = self._read_http(tls) + self.assertTrue(reply.startswith(b'HTTP/1.1 200 OK')) + # same TLS session, second request + tls.sendall(b'POST /api/scp81?req=2 HTTP/1.1\r\n\r\n') reply = self._read_http(tls) self.assertTrue(reply.startswith(b'HTTP/1.1 200 OK')) - self.assertEqual(self._recv(tls), b'') # server closed tls.close() finally: srv.stop() - def test_close_waits_for_drain_callback(self): - # With keep_alive=False and a body, the listener calls on_before_close - # (the server waits for the card to drain the BIP buffer) before - # closing the connection. - seen = [] - + def test_session_end_closes_with_close_notify(self): + # Only the 204 ends the dialog; the server shuts the TLS session down + # cleanly (close_notify while the response is still buffered) and + # then closes the socket. def responder(method, target, headers, body): - return 200, {'X-Admin-Protocol': scp81.GP_PROTOCOL}, b'\x80\x01\x00' + return 204, {'X-Admin-Protocol': scp81.GP_PROTOCOL}, b'' - srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, responder=responder, - on_before_close=lambda peer: seen.append(peer)) + srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, responder=responder) try: tls = self._connect(srv) - client_port = tls.getsockname()[1] tls.sendall(b'POST /api/scp81 HTTP/1.1\r\n\r\n') reply = self._read_http(tls) - self.assertTrue(reply.startswith(b'HTTP/1.1 200 OK')) - self.assertIn(b'Connection: close', reply) - # The server must send close_notify (clean TLS shutdown) before - # closing: unwrap() succeeds only when the peer's close_notify - # has been received. + self.assertIn(b'HTTP/1.1 204 No Content', reply) + # answer the server's close_notify: a mutual clean shutdown means + # unwrap() completes instead of timing out tls.settimeout(3.0) plain = tls.unwrap() - # The close comes after the drain callback: EOF proves it ran. - self.assertEqual(plain.recv(1), b'') - self.assertEqual(len(seen), 1) - self.assertEqual(seen[0][1], client_port) + plain.settimeout(3.0) + self.assertEqual(plain.recv(64), b'') plain.close() finally: srv.stop() @@ -379,8 +373,7 @@ class PskTlsServerTest(unittest.TestCase): 'Content-Type': scp81.GP_CT_COMMAND}, b'\x80\x01\x00') return 204, {'X-Admin-Protocol': scp81.GP_PROTOCOL}, b'' - srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, responder=responder, - keep_alive=True) + srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, responder=responder) try: tls = self._connect(srv) tls.sendall(b'POST /server/adminagent?cmd=1 HTTP/1.1\r\n\r\n') @@ -655,7 +648,7 @@ class BipControlTest(unittest.TestCase): resp = server._scp81_bip_control({ 'action': 'start', 'mode': 'tls', 'host': '127.0.0.1', 'port': 0, 'psk_hex': '00112233', 'psk_identity': 'id-1', - 'chunked': False, 'chunk_size': 100, 'keep_alive': False, + 'chunked': False, 'chunk_size': 100, 'compact_headers': True, 'conn_header': 'close', 'next_uri': '', 'script_template': 'definite', 'cr_tag': True, 'targeted_app': '//aid/A000000151000000', 'link_events': False, @@ -667,8 +660,8 @@ class BipControlTest(unittest.TestCase): self.assertIn('version_seen', listener) self.assertIn('cipher_seen', listener) self.assertEqual((listener['chunked'], listener['chunk_size'], - listener['keep_alive'], listener['compact_headers']), - (False, 100, False, True)) + listener['compact_headers']), + (False, 100, True)) self.assertEqual(server._SCP81_SCRIPT_TEMPLATE, 'definite') self.assertTrue(server._SCP81_SCRIPT_CR_TAG) self.assertEqual(server._SCP81_TARGETED_APP, '//aid/A000000151000000') @@ -946,42 +939,6 @@ class DataAvailableTest(unittest.TestCase): if __name__ == '__main__': unittest.main() -class WaitDrainedTest(unittest.TestCase): - def test_wait_drained_matches_channel_by_port(self): - ch = types.SimpleNamespace(rx=bytearray(), sock=types.SimpleNamespace( - getsockname=lambda: ('127.0.0.1', 40001))) - old = server._BIP - server._BIP = types.SimpleNamespace(channels={1: ch}) - try: - # unknown port / gone channel -> immediate - self.assertIsNone(server._scp81_wait_drained(('127.0.0.1', 40002))) - finally: - server._BIP = old - -class WaitDrainedSlowTest(unittest.TestCase): - def test_wait_drained_waits_for_card_fetch(self): - import threading, time as _time - ch = types.SimpleNamespace(rx=bytearray(), sock=types.SimpleNamespace( - getsockname=lambda: ('127.0.0.1', 40003))) - - def feed(): - _time.sleep(0.15) - ch.rx.extend(b'response-bytes') # pump picks up the response - _time.sleep(0.25) - ch.rx.clear() # card fetches everything - - old = server._BIP - server._BIP = types.SimpleNamespace(channels={1: ch}) - th = threading.Thread(target=feed) - th.start() - t0 = _time.time() - try: - server._scp81_wait_drained(('127.0.0.1', 40003)) - finally: - server._BIP = old - th.join() - self.assertGreater(_time.time() - t0, 0.3) - class KeylogTest(unittest.TestCase): def test_keylog_filename_set(self): import tempfile, os @@ -1001,7 +958,7 @@ class ConnHeaderTest(unittest.TestCase): def responder(method, target, headers, body): return 204, {}, b'' srv = scp81.PskTlsServer('127.0.0.1', 0, PSK, responder=responder, - keep_alive=True, conn_header='none') + conn_header='none') try: ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False