Bambu TLS trust config + finish PrusaLink/PrusaSerial split
Certs: - Vendor Bambu's shared LAN-mode root CA (certs/bambu_ca2.pem, verified self-signed CA:TRUE, see certs/README.md for provenance/fingerprint). - Config gains bambu_ca_cert_path (per-printer override) and bambu_require_valid_cert (the allow/reject flag); printer::bambu::BambuTls turns those into BundledCa/Custom/Insecure. connect() doesn't perform a real handshake yet (no TLS-capable MQTT client wired in), just reports which trust mode it would use. Also reconciles a rename in flight (Printer -> GenericPrinter trait) and finishes the PrusaPrinter -> PrusaLinkPrinter/PrusaSerialPrinter split: added the missing GenericPrinter impl for PrusaSerialPrinter, fixed PrinterHandle's variant payload types, updated mod.rs's pub use list and doc comments, and updated the example to the new 5-variant shape. Verified with cargo check --all-targets (0 errors) and a full run of cargo run --example printer_polymorphism.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
@@ -8,6 +9,16 @@ pub struct Config {
|
||||
pub uplink_url: String,
|
||||
pub go2rtc_bin: String,
|
||||
pub go2rtc_config: String,
|
||||
|
||||
/// Overrides the bundled Bambu root CA (see `certs/bambu_ca2.pem`) with
|
||||
/// a specific certificate file — for a printer whose firmware doesn't
|
||||
/// chain to that shared CA. Most setups leave this unset.
|
||||
pub bambu_ca_cert_path: Option<PathBuf>,
|
||||
/// When `false`, Bambu MQTTS connections skip certificate verification
|
||||
/// entirely instead of checking against the CA above. `true` (the
|
||||
/// default) is the secure choice; only flip this for a printer whose
|
||||
/// certificate you can't get to verify any other way.
|
||||
pub bambu_require_valid_cert: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -19,6 +30,9 @@ impl Config {
|
||||
uplink_url: env_or("CONTINUUM_UPLINK_URL", "wss://api.continuum.local/ws/edge/v1"),
|
||||
go2rtc_bin: env_or("CONTINUUM_GO2RTC_BIN", "go2rtc"),
|
||||
go2rtc_config: env_or("CONTINUUM_GO2RTC_CONFIG", "./go2rtc.yaml"),
|
||||
|
||||
bambu_ca_cert_path: env::var("CONTINUUM_BAMBU_CA_CERT_PATH").ok().map(PathBuf::from),
|
||||
bambu_require_valid_cert: env_or("CONTINUUM_BAMBU_REQUIRE_VALID_CERT", "true") == "true",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+68
-11
@@ -1,37 +1,94 @@
|
||||
use super::{Printer, PrinterBase, PrinterError};
|
||||
use std::path::Path;
|
||||
|
||||
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||
|
||||
/// Bambu's shared LAN-mode root CA ("BBL CA2 RSA") — see certs/README.md
|
||||
/// for how this was obtained and verified. `include_bytes!` embeds it into
|
||||
/// the compiled binary at build time, so there's no file to ship alongside
|
||||
/// the executable.
|
||||
const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem");
|
||||
|
||||
/// Which certificate to trust when connecting to a Bambu printer's MQTTS
|
||||
/// port (8883 — Bambu's LAN mode is TLS-only, there's no unencrypted
|
||||
/// fallback). This replaces a pair of booleans (`use_tls`/`use_ca`) with
|
||||
/// one type that can only represent states that actually make sense.
|
||||
pub enum BambuTls {
|
||||
/// Bambu's shared root CA. Correct for current-generation printers.
|
||||
BundledCa,
|
||||
/// A specific certificate instead — for a printer whose firmware
|
||||
/// doesn't chain to the shared CA.
|
||||
Custom(Vec<u8>),
|
||||
/// Skip certificate verification entirely. The least safe option, only
|
||||
/// reasonable because LAN mode never leaves your local network.
|
||||
Insecure,
|
||||
}
|
||||
|
||||
impl BambuTls {
|
||||
/// Turns `Config`'s two raw settings into one of the three states
|
||||
/// above. `require_valid_cert = false` always means `Insecure`,
|
||||
/// regardless of whether a custom cert path was also given.
|
||||
pub fn from_config(cert_path: Option<&Path>, require_valid_cert: bool) -> anyhow::Result<Self> {
|
||||
if !require_valid_cert {
|
||||
return Ok(BambuTls::Insecure);
|
||||
}
|
||||
match cert_path {
|
||||
Some(path) => Ok(BambuTls::Custom(std::fs::read(path)?)),
|
||||
None => Ok(BambuTls::BundledCa),
|
||||
}
|
||||
}
|
||||
|
||||
/// The CA certificate bytes to hand to the TLS layer — empty when
|
||||
/// `Insecure`, since there's nothing to verify against.
|
||||
pub fn ca_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
BambuTls::BundledCa => BUNDLED_CA,
|
||||
BambuTls::Custom(bytes) => bytes,
|
||||
BambuTls::Insecure => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BambuPrinter {
|
||||
base: PrinterBase,
|
||||
pub access_code: String,
|
||||
tls: BambuTls,
|
||||
}
|
||||
|
||||
impl BambuPrinter {
|
||||
pub fn new(id: &str, name: &str, host: &str, access_code: &str) -> Self {
|
||||
pub fn new(id: &str, name: &str, host: &str, access_code: &str, tls: BambuTls) -> Self {
|
||||
Self {
|
||||
base: PrinterBase { id: id.into(), name: name.into(), host: host.into() },
|
||||
access_code: access_code.into(),
|
||||
tls,
|
||||
}
|
||||
}
|
||||
|
||||
/// Only `BambuPrinter` has this — it's not on the `Printer` trait at
|
||||
/// all, so `PrusaPrinter`/`KlipperPrinter` simply don't have it. This
|
||||
/// is what "adding a printer-specific method" looks like: just define
|
||||
/// it in this struct's own `impl` block.
|
||||
/// Only `BambuPrinter` has this — it's not on the `GenericPrinter`
|
||||
/// trait at all, so the Prusa/Klipper structs simply don't have it.
|
||||
/// This is what "adding a printer-specific method" looks like: just
|
||||
/// define it in this struct's own `impl` block.
|
||||
pub fn dispatch_gcode(&self, file_name: &str) {
|
||||
println!("[{}] uploading {file_name} over FTPS", self.base.name);
|
||||
}
|
||||
}
|
||||
|
||||
impl Printer for BambuPrinter {
|
||||
impl GenericPrinter for BambuPrinter {
|
||||
fn base(&self) -> &PrinterBase {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// OVERRIDE: Bambu's real connect logic opens an MQTTS session (see
|
||||
/// `adapters::bambu::run_telemetry` for that). Here we just print what
|
||||
/// it would do, so the focus stays on how dispatch works.
|
||||
/// OVERRIDE: Bambu's real connect logic opens an MQTTS session on
|
||||
/// 8883, presenting `self.tls.ca_bytes()` to the TLS layer to verify
|
||||
/// (or, for `BambuTls::Insecure`, skipping verification). Wiring that
|
||||
/// up needs a TLS-capable MQTT client (e.g. rumqttc + rustls) — not
|
||||
/// added back yet, so this just reports which trust mode it would use.
|
||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||
println!("[{}] connecting over MQTT with access code {}", self.base.name, self.access_code);
|
||||
let mode = match &self.tls {
|
||||
BambuTls::BundledCa => "bundled Bambu CA",
|
||||
BambuTls::Custom(_) => "custom CA cert",
|
||||
BambuTls::Insecure => "no certificate verification",
|
||||
};
|
||||
println!("[{}] connecting over MQTTS:8883 ({mode})", self.base.name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Printer, PrinterBase, PrinterError};
|
||||
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||
|
||||
pub struct KlipperPrinter {
|
||||
base: PrinterBase,
|
||||
@@ -12,7 +12,7 @@ impl KlipperPrinter {
|
||||
}
|
||||
}
|
||||
|
||||
impl Printer for KlipperPrinter {
|
||||
impl GenericPrinter for KlipperPrinter {
|
||||
fn base(&self) -> &PrinterBase {
|
||||
&self.base
|
||||
}
|
||||
|
||||
+31
-17
@@ -1,5 +1,5 @@
|
||||
//! Printer hierarchy — the Rust answer to
|
||||
//! `GenericPrinter -> BambuPrinter/PrusaPrinter/KlipperPrinter`-style
|
||||
//! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-style
|
||||
//! inheritance, since Rust structs can't `extend` each other.
|
||||
//!
|
||||
//! Three pieces, each doing one job an OOP base class would normally do:
|
||||
@@ -7,10 +7,10 @@
|
||||
//! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every
|
||||
//! printer struct below *has* one of these (composition) instead of
|
||||
//! *extending* one.
|
||||
//! 2. `Printer` — a trait holding the shared *behavior*. `connect()` has no
|
||||
//! default body, so every printer type is forced to write its own —
|
||||
//! that's what "overriding" looks like when there's no inherited body to
|
||||
//! override in the first place.
|
||||
//! 2. `GenericPrinter` — a trait holding the shared *behavior*. `connect()`
|
||||
//! has no default body, so every printer type is forced to write its
|
||||
//! own — that's what "overriding" looks like when there's no inherited
|
||||
//! body to override in the first place.
|
||||
//! 3. `PrinterHandle` — an enum listing the closed set of printer kinds.
|
||||
//! Stands in for "any subclass of GenericPrinter".
|
||||
//!
|
||||
@@ -25,9 +25,9 @@ mod bambu;
|
||||
mod klipper;
|
||||
mod prusa;
|
||||
|
||||
pub use bambu::BambuPrinter;
|
||||
pub use bambu::{BambuPrinter, BambuTls};
|
||||
pub use klipper::KlipperPrinter;
|
||||
pub use prusa::PrusaPrinter;
|
||||
pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter};
|
||||
|
||||
/// A tiny error type: just a message. `String` would work too — this exists
|
||||
/// mainly so `?` has something concrete to convert into everywhere.
|
||||
@@ -43,9 +43,9 @@ pub struct PrinterBase {
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
/// The shared interface — think `interface Printer` (Java/TS) or an ABC
|
||||
/// (Python). Every vendor's struct implements this.
|
||||
pub trait Printer {
|
||||
/// The shared interface — think `interface GenericPrinter` (Java/TS) or an
|
||||
/// ABC (Python). Every vendor's struct implements this.
|
||||
pub trait GenericPrinter {
|
||||
/// Read-only access to the shared fields.
|
||||
fn base(&self) -> &PrinterBase;
|
||||
|
||||
@@ -69,10 +69,20 @@ pub trait Printer {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PrinterFlavour {
|
||||
BambuV1Printer,
|
||||
BambuV2Printer,
|
||||
PrusaLinkPrinter,
|
||||
PrusaSerialPrinter,
|
||||
KlipperPrinter,
|
||||
}
|
||||
|
||||
/// The closed set of printer kinds this fleet can talk to.
|
||||
pub enum PrinterHandle {
|
||||
Bambu(BambuPrinter),
|
||||
Prusa(PrusaPrinter),
|
||||
BambuV1(BambuPrinter),
|
||||
BambuV2(BambuPrinter),
|
||||
PrusaLink(PrusaLinkPrinter),
|
||||
PrusaSerial(PrusaSerialPrinter),
|
||||
Klipper(KlipperPrinter),
|
||||
}
|
||||
|
||||
@@ -80,19 +90,23 @@ pub enum PrinterHandle {
|
||||
// and forwards to that variant's own implementation. This one block is the
|
||||
// only "boilerplate tax" for not having inheritance — everything else reads
|
||||
// like normal code.
|
||||
impl Printer for PrinterHandle {
|
||||
impl GenericPrinter for PrinterHandle {
|
||||
fn base(&self) -> &PrinterBase {
|
||||
match self {
|
||||
PrinterHandle::Bambu(p) => p.base(),
|
||||
PrinterHandle::Prusa(p) => p.base(),
|
||||
PrinterHandle::BambuV1(p) => p.base(),
|
||||
PrinterHandle::BambuV2(p) => p.base(),
|
||||
PrinterHandle::PrusaLink(p) => p.base(),
|
||||
PrinterHandle::PrusaSerial(p) => p.base(),
|
||||
PrinterHandle::Klipper(p) => p.base(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||
match self {
|
||||
PrinterHandle::Bambu(p) => p.connect().await,
|
||||
PrinterHandle::Prusa(p) => p.connect().await,
|
||||
PrinterHandle::BambuV1(p) => p.connect().await,
|
||||
PrinterHandle::BambuV2(p) => p.connect().await,
|
||||
PrinterHandle::PrusaLink(p) => p.connect().await,
|
||||
PrinterHandle::PrusaSerial(p) => p.connect().await,
|
||||
PrinterHandle::Klipper(p) => p.connect().await,
|
||||
}
|
||||
}
|
||||
|
||||
+38
-4
@@ -1,11 +1,13 @@
|
||||
use super::{Printer, PrinterBase, PrinterError};
|
||||
use super::{GenericPrinter, PrinterBase, PrinterError};
|
||||
|
||||
pub struct PrusaPrinter {
|
||||
/****************** PrusaLink ******************/
|
||||
|
||||
pub struct PrusaLinkPrinter {
|
||||
base: PrinterBase,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
impl PrusaPrinter {
|
||||
impl PrusaLinkPrinter {
|
||||
pub fn new(id: &str, name: &str, host: &str, api_key: &str) -> Self {
|
||||
Self {
|
||||
base: PrinterBase { id: id.into(), name: name.into(), host: host.into() },
|
||||
@@ -14,7 +16,7 @@ impl PrusaPrinter {
|
||||
}
|
||||
}
|
||||
|
||||
impl Printer for PrusaPrinter {
|
||||
impl GenericPrinter for PrusaLinkPrinter {
|
||||
fn base(&self) -> &PrinterBase {
|
||||
&self.base
|
||||
}
|
||||
@@ -27,3 +29,35 @@ impl Printer for PrusaPrinter {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/****************** PrusaSerial ******************/
|
||||
|
||||
pub struct PrusaSerialPrinter {
|
||||
base: PrinterBase,
|
||||
pub com_port: i16,
|
||||
}
|
||||
|
||||
impl PrusaSerialPrinter {
|
||||
pub fn new(id: &str, name: &str, com_port: i16) -> Self {
|
||||
Self {
|
||||
base: PrinterBase { id: id.into(), name: name.into(), host: "127.0.0.1".to_string() },
|
||||
com_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericPrinter for PrusaSerialPrinter {
|
||||
fn base(&self) -> &PrinterBase {
|
||||
&self.base
|
||||
}
|
||||
|
||||
/// OVERRIDE: a fourth, again-different body — no network host at all,
|
||||
/// just a local serial port (real version: open the port, e.g. via the
|
||||
/// `serialport`/`tokio-serial` crate, and speak Prusa's G-code-over-
|
||||
/// serial protocol).
|
||||
async fn connect(&mut self) -> Result<(), PrinterError> {
|
||||
println!("[{}] connecting over serial (COM{})", self.base.name, self.com_port);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user