You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add server-side plumbing so an operator can force the Socket.IO transport list
the client uses — specifically to force HTTP long-polling for deployments behind
WebSocket-blocking corporate proxies / load balancers. The gateway resolves an
operator setting into socket.transports and injects it into the runtime
config; the client (billchurch/webssh2_client#131)
reads and honors it.
This is the correct-layer replacement for #548, an external contribution that
solved the symptom by regex-patching the minified server-served bundle at
request time. That approach was fragile (silently no-ops if minified output
shape changes), untested, and inadvertently flipped the no-config default to
polling-first. #548 is being closed in favor of this server change + client #131.
Functionally the transport feature is channel-agnostic — it just adds a transports field to the config object that injectConfig serializes, so it
rides whichever channel is live with no change to buildSocketConfig.
But both changes touch the app/utils/html-transformer.ts area, and we want
the going-forward JSON-block channel to be the established contract before we
document a new injected field on top of it.
Decided surface: config.json (options.transport) + WEBSSH2_OPTIONS_TRANSPORT
env var only. No ?transport= URL parameter — a per-request override would let
any client force polling-first, a needless performance-manipulation surface.
Value semantics (shared contract with client #131)
Operator sets an order-significant transport list, validated against the
whitelist {websocket, polling}. Delivered as socket.transports on the
injected runtime config.
long-polling only — correct for proxies that block WS upgrades
websocket,polling
['websocket', 'polling']
explicit default
polling,websocket
['polling', 'websocket']
polling-first, upgrade if possible
Rules:
Unset → omit the field entirely. The client's existing default
(['websocket', 'polling']) stays the single source of truth — never silently
flip to polling-first (adds a round-trip to every existing deployment).
Invalid/empty → log a warning and omit. Fail safe to the default rather
than break the connection.
Server requirements
Config + env surface. Add transport to OptionsConfig
(app/types/config.ts) and the env mapping WEBSSH2_OPTIONS_TRANSPORT.
Accept both string and array forms in config.json: "transport": "polling" (string, fix long polling fallback #548's shape) or "transport": ["polling", "websocket"]. The env var is always a
comma-separated string. Both normalize through one parser.
Non-throwing pure parser. Parse comma-separated (or array) → trim,
lowercase, whitelist-filter to {websocket, polling}, dedupe, preserve order
→ string[] | undefined. It must never throw on bad input (return undefined + warn) so a malformed value cannot crash config load or a
connection. Wire WEBSSH2_OPTIONS_TRANSPORT as bespoke env handling (the
pattern WEBSSH2_THEMING_THEMES uses) — not the generic boolean ENV_VAR_MAPPING path, which would store the raw unvalidated string.
Normalize once, at config-load time.
Inject conditionally. Thread cfg into buildSocketConfig
(app/connectionHandler.ts:53). buildTempConfig already holds cfg and
passes it to buildHeaderConfig (connectionHandler.ts:240) — pass it to buildSocketConfig(req, isTelnet, cfg) the same way. Add transports into
the socket fragment only when the resolved list is a non-empty array;
unset/invalid → omit the key. Never emit [] — with the client's reconnection: false, an empty transport list yields a socket that can never
connect.
No edit to html-transformer.ts is required: adding transports to the socket
fragment is auto-serialized by injectConfig, and its existing script-safe
escaping (< → <, U+2028/U+2029) already covers the string-array value.
Acceptance
options.transport / WEBSSH2_OPTIONS_TRANSPORT accepted, validated, and
normalized per the table above; env overrides config per existing precedence.
Invalid/empty input never throws and never emits transports: [] — the field
is omitted so the client keeps its default.
buildSocketConfig emits socket.transports only for a non-empty resolved
list.
Unit tests for the parser (string form, array form, mixed valid/invalid,
all-invalid, empty, casing/whitespace, dedupe, order preservation) and for buildSocketConfig (omit vs inject).
The client (#131)
will also whitelist-filter the injected transports before passing them to io() — the target environment is by definition a meddling proxy, so both sides
validate. The server change here is the authoritative source; the client filter
is a safety net.
Summary
Add server-side plumbing so an operator can force the Socket.IO transport list
the client uses — specifically to force HTTP long-polling for deployments behind
WebSocket-blocking corporate proxies / load balancers. The gateway resolves an
operator setting into
socket.transportsand injects it into the runtimeconfig; the client (billchurch/webssh2_client#131)
reads and honors it.
This is the correct-layer replacement for #548, an external contribution that
solved the symptom by regex-patching the minified server-served bundle at
request time. That approach was fragile (silently no-ops if minified output
shape changes), untested, and inadvertently flipped the no-config default to
polling-first. #548 is being closed in favor of this server change + client #131.
This work must land after #546 ("adopt JSON config block injection, deprecate
inline
window.webssh2Configscript, and ship a CSP").Why the ordering:
runtime config object. Today that object reaches the client only via the
legacy
window.webssh2Configglobal; feat(security): adopt JSON config block injection, deprecate inline window.webssh2Config script, and ship a CSP #546 Phase 1 makes the inert<script type="application/json" id="webssh2-config">block the supported,going-forward channel.
transportsfield to the config object thatinjectConfigserializes, so itrides whichever channel is live with no change to
buildSocketConfig.But both changes touch the
app/utils/html-transformer.tsarea, and we wantthe going-forward JSON-block channel to be the established contract before we
document a new injected field on top of it.
JSON block) instead of describing then immediately deprecating the legacy one.
Do not start this until #546 Phase 1 is merged.
Scope
Decided surface: config.json (
options.transport) +WEBSSH2_OPTIONS_TRANSPORTenv var only. No
?transport=URL parameter — a per-request override would letany client force polling-first, a needless performance-manipulation surface.
Value semantics (shared contract with client #131)
Operator sets an order-significant transport list, validated against the
whitelist
{websocket, polling}. Delivered assocket.transportson theinjected runtime config.
socket.transports['websocket', 'polling'](websocket-first)polling['polling']websocket,polling['websocket', 'polling']polling,websocket['polling', 'websocket']Rules:
(
['websocket', 'polling']) stays the single source of truth — never silentlyflip to polling-first (adds a round-trip to every existing deployment).
than break the connection.
Server requirements
Config + env surface. Add
transporttoOptionsConfig(
app/types/config.ts) and the env mappingWEBSSH2_OPTIONS_TRANSPORT."transport": "polling"(string, fix long polling fallback #548's shape) or"transport": ["polling", "websocket"]. The env var is always acomma-separated string. Both normalize through one parser.
Non-throwing pure parser. Parse comma-separated (or array) → trim,
lowercase, whitelist-filter to
{websocket, polling}, dedupe, preserve order→
string[] | undefined. It must never throw on bad input (returnundefined+ warn) so a malformed value cannot crash config load or aconnection. Wire
WEBSSH2_OPTIONS_TRANSPORTas bespoke env handling (thepattern
WEBSSH2_THEMING_THEMESuses) — not the generic booleanENV_VAR_MAPPINGpath, which would store the raw unvalidated string.Normalize once, at config-load time.
Inject conditionally. Thread
cfgintobuildSocketConfig(
app/connectionHandler.ts:53).buildTempConfigalready holdscfgandpasses it to
buildHeaderConfig(connectionHandler.ts:240) — pass it tobuildSocketConfig(req, isTelnet, cfg)the same way. Addtransportsintothe
socketfragment only when the resolved list is a non-empty array;unset/invalid → omit the key. Never emit
[]— with the client'sreconnection: false, an empty transport list yields a socket that can neverconnect.
No edit to
html-transformer.tsis required: addingtransportsto the socketfragment is auto-serialized by
injectConfig, and its existing script-safeescaping (
<→<, U+2028/U+2029) already covers the string-array value.Acceptance
options.transport/WEBSSH2_OPTIONS_TRANSPORTaccepted, validated, andnormalized per the table above; env overrides config per existing precedence.
transports: []— the fieldis omitted so the client keeps its default.
buildSocketConfigemitssocket.transportsonly for a non-empty resolvedlist.
all-invalid, empty, casing/whitespace, dedupe, order preservation) and for
buildSocketConfig(omit vs inject).options.transportand the env var, ontop of the feat(security): adopt JSON config block injection, deprecate inline window.webssh2Config script, and ship a CSP #546 JSON-block injection contract.
Defense in depth
The client (#131)
will also whitelist-filter the injected transports before passing them to
io()— the target environment is by definition a meddling proxy, so both sidesvalidate. The server change here is the authoritative source; the client filter
is a safety net.
References
env parser), A3 (string+array config forms), S3 (client re-validation)
incorporated above.