Skip to content

feat: honor operator-configured Socket.IO transport (server plumbing for long-polling fallback) #549

Description

@billchurch

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.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.

⚠️ Depends on #546 (complete first)

This work must land after #546 ("adopt JSON config block injection, deprecate
inline window.webssh2Config script, and ship a CSP").

Why the ordering:

Do not start this until #546 Phase 1 is merged.

Scope

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.

Setting Resolved socket.transports Effect
unset (default) field omitted client keeps ['websocket', 'polling'] (websocket-first)
polling ['polling'] 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

  1. 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.
  2. 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.

  3. 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).
  • Docs (README / config docs) describe options.transport and the env var, on
    top 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 sides
validate. The server change here is the authoritative source; the client filter
is a safety net.

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions