Files
bambuddy/backend/app/services/ftp_profiles.py
T
maziggy 77f8a3a3ff fix(tls): declare TLS 1.2 as the minimum for printer FTPS and MQTT
ssl.create_default_context() leaves minimum_version at MINIMUM_SUPPORTED,
so the floor came from the OpenSSL build rather than from Bambuddy. On
identical OpenSSL 3.5.6, python:3.13-slim-trixie reports TLSv1_2 while a
bare-metal venv reports MINIMUM_SUPPORTED -- Docker installs were floored
at 1.2, bare-metal and appliance installs were not.

Set minimum_version explicitly in ImplicitFTP_TLS and the MQTT client. On
the P2S/X2D profiles that also cap maximum_version this becomes an exact
TLS 1.2 pin. Probed against an X1C and an H2D on :990 and :8883: both
complete only on TLS 1.2 and reject 1.0, 1.1 and 1.3; live FTPS login
through the new path succeeds on both.

Also correct a stale comment in ftp_profiles.py -- X1C and H2D refuse
TLS 1.3, so cap_tls_v1_2 is a no-op there, contrary to what it claimed.
2026-07-10 07:55:14 +02:00

117 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Per-printer-model FTP tuning knobs.
Mirrors the shape of :mod:`backend.app.services.camera_profiles` — a
small registry of per-model overrides so quirky firmwares can be
tuned without sprinkling ``if model == "X":`` branches through
``bambu_ftp.py``. Adding a new model's quirk is a config edit (an
entry in ``_PROFILES`` plus the alias for its internal SSDP code if
needed), not another hard-coded branch.
The default profile matches the historical pre-fix behaviour, so
every model that doesn't have an entry here keeps its existing FTP
behaviour byte-for-byte.
Currently only the TLS-version cap lives here (P2S firmware
01.02.00.00 needs it — see ``cap_tls_v1_2`` below). The A1
data-channel-plaintext quirk still lives in :class:`BambuFTPClient`
via ``A1_MODELS`` / ``skip_session_reuse``; folding that into a
profile field is a future cleanup, not load-bearing for this fix.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class FTPProfile:
"""Tuning knobs for one printer model's FTP path.
All defaults reflect the historical behaviour. Models with quirky
firmware override individual fields rather than re-defining the
whole profile.
"""
# Pin the SSL context's ``maximum_version`` to TLS 1.2.
#
# ``ssl.create_default_context()`` negotiates TLS 1.3 when both peers
# support it. Some Bambu printer firmwares (P2S 01.02.00.00 confirmed
# by @iitazz, #1401) implement session reuse on the FTPS data
# channel against an old vsFTPd build that doesn't tolerate TLS
# 1.3's asynchronous session-ticket model: the data channel gets
# torn down mid-stream and the upload aborts with 426 "Failure
# reading network stream" — visible as a clean truncation at a
# chunk boundary (one reporter saw exactly 7 × 64 KB landed on
# the printer). Capping to TLS 1.2 makes session resumption
# synchronous and the upload completes normally.
#
# Note this cap only bites on models that *offer* 1.3 in the first
# place. Probed directly on :990, an X1C and an H2D both refuse
# TLS 1.0, 1.1 and 1.3 with a handshake_failure alert and complete
# only on 1.2 — so for those models the cap is a no-op and the
# negotiated version was never 1.3. The P2S evidently does offer
# 1.3, which is why it alone surfaced the session-reuse bug.
# (P1S untested; no claim made either way.)
#
# **Defaults to False** — only applied to printer models where a
# reporter has confirmed the symptom. This is deliberately
# conservative; flipping a printer to the capped path is a config
# edit when a new model surfaces the same bug.
cap_tls_v1_2: bool = False
# ---------------------------------------------------------------------------
# Profile registry
# ---------------------------------------------------------------------------
# Default profile = historical behaviour. Used for every model that
# doesn't have an entry in ``_PROFILES``.
DEFAULT_PROFILE = FTPProfile()
# Per-model overrides. Keys are uppercase display names (e.g. "P2S")
# AFTER alias normalisation, so internal SSDP codes ("N7") resolve via
# ``_MODEL_ALIASES`` below.
_PROFILES: dict[str, FTPProfile] = {
# P2S firmware 01.02.00.00 trips the vsFTPd + TLS 1.3 session-reuse
# bug on the FTPS data channel (#1401, reporter @iitazz). Cap to
# TLS 1.2 so session resumption is synchronous and the upload
# completes.
"P2S": FTPProfile(
cap_tls_v1_2=True,
),
# X2D firmware 01.01.00.00 fails the implicit-FTPS handshake on
# port 990 with ``[SSL: WRONG_VERSION_NUMBER]`` against Python
# 3.13's default TLS-1.3 ClientHello (#1638, reporter @vasmarfas).
# Without the 3MF download the print falls through to the no-3MF
# fallback archive path and the card lands almost empty (no
# filament total, no layers, no MakerWorld link). Cap to TLS 1.2
# by analogy with P2S; if the symptom turns out to be a different
# FTPS variant on the X2D (explicit AUTH TLS, different port) the
# entry stays useful as a per-model tuning slot for the follow-up.
"X2D": FTPProfile(
cap_tls_v1_2=True,
),
}
# SSDP internal codes that should resolve to a display-name profile.
# Mirrors the same map in :mod:`camera_profiles`.
_MODEL_ALIASES: dict[str, str] = {
"N7": "P2S", # P2S internal SSDP code
"N6": "X2D", # X2D internal SSDP code
}
def get_ftp_profile(model: str | None) -> FTPProfile:
"""Return the :class:`FTPProfile` for *model*, or the default.
``model`` can be either a display name (e.g. ``"P2S"``) or an
internal SSDP code (e.g. ``"N7"``). Unknown / missing models fall
back to :data:`DEFAULT_PROFILE` so the FTP path is never blocked
on a missing entry.
"""
if not model:
return DEFAULT_PROFILE
key = model.upper().strip()
key = _MODEL_ALIASES.get(key, key)
return _PROFILES.get(key, DEFAULT_PROFILE)