From d8b7430296080ac3efe9fdee1cfd023b35133666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Fri, 28 Aug 2026 20:48:05 +0000 Subject: [PATCH] Add printers.toml (gitignored) + real TLS cert test against actual hardware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New: - printers.example.toml (committed template) / printers.toml (gitignored, real hosts + access codes don't belong in git) — a list of real printers with vendor, host, access_code/api_key/com_port, and an optional per-printer CA override. - src/fleet.rs loads that file into ready-to-use PrinterHandles, matching vendor strings to the right constructor. - BambuPrinter::test_tls_handshake() does a real (blocking, one-shot) TLS handshake to port 8883 using that printer's configured BambuTls trust mode — no MQTT protocol, just 'does the certificate verify'. Added native-tls and toml as direct dependencies for this. - examples/test_bambu_certs.rs loads printers.toml and runs the handshake test against every Bambu entry. Also fixes a real bug found while testing against unreachable IPs: plain TcpStream::connect has no timeout and hung indefinitely on an offline printer — switched to connect_timeout (5s). Verified with cargo check --all-targets (0 errors) and by actually running test_bambu_certs against a local printers.toml (correctly errored on a missing cert file, then correctly timed out against unreachable test IPs instead of hanging). --- .gitignore | 1 + Cargo.toml | 6 +++ README.md | 19 ++++++-- examples/test_bambu_certs.rs | 26 +++++++++++ printers.example.toml | 35 +++++++++++++++ src/fleet.rs | 84 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/printer/bambu.rs | 37 ++++++++++++++++ 8 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 examples/test_bambu_certs.rs create mode 100644 printers.example.toml create mode 100644 src/fleet.rs diff --git a/.gitignore b/.gitignore index 55e567b..800ca46 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ Cargo.lock *.sqlite3 .env *.log +printers.toml diff --git a/Cargo.toml b/Cargo.toml index 11ef271..06f4f00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,18 @@ path = "src/main.rs" name = "printer_polymorphism" path = "examples/printer_polymorphism.rs" +[[example]] +name = "test_bambu_certs" +path = "examples/test_bambu_certs.rs" + [dependencies] tokio = { version = "1.40", features = ["full"] } tokio-tungstenite = { version = "0.24", features = ["native-tls"] } futures-util = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" +toml = "0.8" +native-tls = "0.2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } anyhow = "1" diff --git a/README.md b/README.md index 9ce5a14..c412ebf 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ inheritance) on something simple before adding real networking on top. cp .env.example .env cargo run # the daemon: cloud uplink + go2rtc watchdog cargo run --example printer_polymorphism # standalone demo, no network/env needed + +cp printers.example.toml printers.toml # fill in your real printers (gitignored) +cargo run --example test_bambu_certs # real TLS handshake test against each Bambu printer ``` ## Structure @@ -27,16 +30,26 @@ cargo run --example printer_polymorphism # standalone demo, no network/env nee src/ main.rs Runs the uplink and the go2rtc watchdog side by side config.rs Loads settings from environment variables + fleet.rs Loads printers.toml into ready-to-use PrinterHandles uplink/ WebSocket client to continuum-backend: connect, heartbeat, reconnect on drop - printer/ Printer trait + PrinterBase + PrinterHandle enum + one stub per vendor + printer/ GenericPrinter trait + PrinterBase + PrinterHandle enum + one stub per vendor go2rtc.rs Restarts the go2rtc camera-restreaming process if it dies examples/ - printer_polymorphism.rs Runs all three printer stubs through one `connect()` call site + printer_polymorphism.rs Runs all five printer stubs through one `connect()` call site + test_bambu_certs.rs Loads printers.toml, does a real TLS handshake to each Bambu printer ``` `src/printer/` is where the "inheritance" question lives — see that module's doc comment for the trait+enum pattern this project uses instead -of class inheritance, and run the example above to see it work. +of class inheritance, and run `printer_polymorphism` to see it work. + +`printers.toml` (gitignored — copy from `printers.example.toml`) holds real +per-printer connection details: host, access code, and — since not every +Bambu printer trusts the same certificate (see `certs/README.md`) — an +optional per-printer CA override. `src/fleet.rs` loads it; nothing in +`main.rs` uses it yet, but `test_bambu_certs` does, as a real (if narrow — +just the TLS handshake, no MQTT) way to check a printer's certificate +without needing the full MQTT client built yet. ## What's not here yet (on purpose) diff --git a/examples/test_bambu_certs.rs b/examples/test_bambu_certs.rs new file mode 100644 index 0000000..3fa8e29 --- /dev/null +++ b/examples/test_bambu_certs.rs @@ -0,0 +1,26 @@ +//! Run with: cargo run --example test_bambu_certs +//! +//! Loads printers.toml (copy printers.example.toml to get started, fill in +//! your real printers) and attempts a raw TLS handshake to each Bambu +//! printer's MQTTS port (8883) — no MQTT protocol involved, just "does the +//! certificate verify". Prusa/Klipper entries are skipped; they don't have +//! this trust question. + +use continuum_proxy::fleet; +use continuum_proxy::printer::{GenericPrinter, PrinterHandle}; + +fn main() -> anyhow::Result<()> { + let printers = fleet::load(std::path::Path::new("printers.toml"))?; + + for printer in &printers { + if let PrinterHandle::BambuV1(bambu) | PrinterHandle::BambuV2(bambu) = printer { + print!("{}: ", bambu.base().name); + match bambu.test_tls_handshake() { + Ok(()) => println!("OK — certificate verified"), + Err(err) => println!("FAILED — {err}"), + } + } + } + + Ok(()) +} diff --git a/printers.example.toml b/printers.example.toml new file mode 100644 index 0000000..d9af7e0 --- /dev/null +++ b/printers.example.toml @@ -0,0 +1,35 @@ +# Copy this file to printers.toml (gitignored) and fill in your real +# printers. See src/fleet.rs for the loader, examples/test_bambu_certs.rs +# for a way to test a Bambu printer's certificate against this config. + +[[printer]] +id = "p1p" +name = "P1P" +vendor = "bambu_v1" # bambu_v1 | bambu_v2 | prusa_link | prusa_serial | klipper +host = "192.168.1.50" +access_code = "REPLACE_WITH_ACCESS_CODE" +# P1P doesn't chain to the bundled CA — point this at a cert you've +# downloaded from the printer itself. Leave unset to use the bundled CA. +ca_cert_path = "./certs/p1p.pem" + +[[printer]] +id = "h2c" +name = "H2C" +vendor = "bambu_v2" +host = "192.168.1.51" +access_code = "REPLACE_WITH_ACCESS_CODE" +# No ca_cert_path — current-generation printers verify against the bundled +# CA (certs/bambu_ca2.pem) fine. + +# [[printer]] +# id = "voron" +# name = "Voron 2.4" +# vendor = "klipper" +# host = "192.168.1.52" + +# [[printer]] +# id = "mk4" +# name = "MK4" +# vendor = "prusa_link" +# host = "192.168.1.53" +# api_key = "REPLACE_WITH_API_KEY" diff --git a/src/fleet.rs b/src/fleet.rs new file mode 100644 index 0000000..f3de1ca --- /dev/null +++ b/src/fleet.rs @@ -0,0 +1,84 @@ +//! Loads real printer connection details from a TOML file — see +//! `printers.example.toml` for the shape. The actual file (`printers.toml` +//! by default) is gitignored: it holds real hostnames and access codes for +//! your printers, which don't belong in version control. + +use std::path::Path; + +use serde::Deserialize; + +use crate::printer::{BambuPrinter, BambuTls, KlipperPrinter, PrinterHandle, PrusaLinkPrinter, PrusaSerialPrinter}; + +#[derive(Debug, Deserialize)] +struct FleetFile { + #[serde(rename = "printer", default)] + printers: Vec, +} + +#[derive(Debug, Deserialize)] +struct PrinterEntry { + id: String, + name: String, + /// One of: "bambu_v1", "bambu_v2", "prusa_link", "prusa_serial", "klipper". + vendor: String, + host: Option, + access_code: Option, + api_key: Option, + com_port: Option, + /// Path to a printer-specific CA certificate. Leave unset to use the + /// bundled Bambu CA — only needed for a printer that doesn't chain to + /// it (P1P, at least; see certs/README.md). + ca_cert_path: Option, + #[serde(default = "default_true")] + require_valid_cert: bool, +} + +fn default_true() -> bool { + true +} + +/// Reads and parses a printers file into ready-to-use `PrinterHandle`s. +pub fn load(path: &Path) -> anyhow::Result> { + let text = std::fs::read_to_string(path).map_err(|err| { + anyhow::anyhow!( + "failed to read {path:?}: {err} (copy printers.example.toml to printers.toml and fill in your printers)" + ) + })?; + let file: FleetFile = toml::from_str(&text)?; + + file.printers.into_iter().map(build_printer).collect() +} + +fn build_printer(entry: PrinterEntry) -> anyhow::Result { + match entry.vendor.as_str() { + "bambu_v1" | "bambu_v2" => { + let host = require_field(&entry.id, "host", entry.host)?; + let access_code = require_field(&entry.id, "access_code", entry.access_code)?; + let tls = BambuTls::resolve(entry.ca_cert_path.as_deref().map(Path::new), entry.require_valid_cert)?; + let printer = BambuPrinter::new(&entry.id, &entry.name, &host, &access_code, tls); + Ok(if entry.vendor == "bambu_v1" { + PrinterHandle::BambuV1(printer) + } else { + PrinterHandle::BambuV2(printer) + }) + } + "prusa_link" => { + let host = require_field(&entry.id, "host", entry.host)?; + let api_key = require_field(&entry.id, "api_key", entry.api_key)?; + Ok(PrinterHandle::PrusaLink(PrusaLinkPrinter::new(&entry.id, &entry.name, &host, &api_key))) + } + "prusa_serial" => { + let com_port = entry.com_port.ok_or_else(|| anyhow::anyhow!("{}: prusa_serial needs com_port", entry.id))?; + Ok(PrinterHandle::PrusaSerial(PrusaSerialPrinter::new(&entry.id, &entry.name, com_port))) + } + "klipper" => { + let host = require_field(&entry.id, "host", entry.host)?; + Ok(PrinterHandle::Klipper(KlipperPrinter::new(&entry.id, &entry.name, &host))) + } + other => Err(anyhow::anyhow!("{}: unknown vendor '{other}'", entry.id)), + } +} + +fn require_field(printer_id: &str, field: &str, value: Option) -> anyhow::Result { + value.ok_or_else(|| anyhow::anyhow!("{printer_id}: missing required field '{field}'")) +} diff --git a/src/lib.rs b/src/lib.rs index 5f6f319..8e3e076 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,4 +3,5 @@ //! `examples/`) and future integration tests can `use continuum_proxy::...` //! without duplicating module declarations. +pub mod fleet; pub mod printer; diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index d36531f..f2832f8 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -49,6 +49,20 @@ impl BambuTls { BambuTls::Insecure => &[], } } + + /// Builds a `native_tls` connector configured for this trust mode: + /// trusting only `ca_bytes()`, or accepting anything for `Insecure`. + fn build_connector(&self) -> anyhow::Result { + let mut builder = native_tls::TlsConnector::builder(); + + if matches!(self, BambuTls::Insecure) { + builder.danger_accept_invalid_certs(true); + } else { + builder.add_root_certificate(native_tls::Certificate::from_pem(self.ca_bytes())?); + } + + Ok(builder.build()?) + } } pub struct BambuPrinter { @@ -73,6 +87,29 @@ impl BambuPrinter { pub fn dispatch_gcode(&self, file_name: &str) { println!("[{}] uploading {file_name} over FTPS", self.base.name); } + + /// Attempts a raw TLS handshake to this printer's MQTTS port — no MQTT + /// protocol at all, just "does the certificate verify against this + /// printer's configured trust mode". This is a real network call + /// (blocking, since it's a one-shot diagnostic rather than part of the + /// async daemon), useful for testing a printer's certificate before + /// building a real MQTT client on top. + pub fn test_tls_handshake(&self) -> anyhow::Result<()> { + use std::net::ToSocketAddrs; + + let addr = (self.base.host.as_str(), 8883) + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow::anyhow!("could not resolve {}", self.base.host))?; + + // A plain TcpStream::connect has no timeout — an offline printer + // would hang this call forever instead of failing. 5s is plenty + // for a LAN. + let stream = std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(5))?; + let connector = self.tls.build_connector()?; + connector.connect(&self.base.host, stream)?; + Ok(()) + } } impl GenericPrinter for BambuPrinter {