From 23a289e4fa809c72b0ce4220619b4edf2bd2108d Mon Sep 17 00:00:00 2001 From: Stewart Allen Date: Mon, 3 Feb 2025 12:47:03 -0500 Subject: [PATCH] add proxy debug tool for bambu printers --- mods/bambu/cli.js | 4 + mods/bambu/proxy.js | 213 ++++++++++++++++++++++++++++++++++++++++++++ mods/package.json | 2 +- 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 mods/bambu/proxy.js diff --git a/mods/bambu/cli.js b/mods/bambu/cli.js index 207dad47..d0d1d074 100644 --- a/mods/bambu/cli.js +++ b/mods/bambu/cli.js @@ -162,6 +162,10 @@ client.on("connect", () => { }); }); +client.on("error", (error) => { + log("mqtt error", error); +}); + client.on("message", (topic, message) => { log(JSON.parse(message.toString())); }); diff --git a/mods/bambu/proxy.js b/mods/bambu/proxy.js new file mode 100644 index 00000000..b9733c06 --- /dev/null +++ b/mods/bambu/proxy.js @@ -0,0 +1,213 @@ +/** + * Utility to proxy local MQTT requests to a remote Bambu printer + * and intercept / display communications. + * + * Bambu's network plugin will not connect to this process unless + * you use the private key and certs extracted from Bambu Connect + * following the instructions here: + * + * https://wiki.rossmanngroup.com/wiki/Reverse_engineering_Bambu_Connect + * + * The private key is clearly delineated. Put the contents into the + * server-key.pem file. But you need to concatenate all of the certs + * after the private key into the server-cert.pem + */ + +let util = require('util'); +let args = process.argv.slice(2); + +if (args.length !== 5) { + console.log([ + 'usage: proxy [local] [name] [host] [code] [serial-no]', + 'where:', + ' local = local ip to broadcast (this host)', + ' name = name of printer to appear in slicer', + ' host = host name or IP address of proxied printer', + ' code = LAN mode code of proxied printer', + ' serial = proxied printer serial #' + ].join('\n')); + return process.exit(0); +} + +function log() { + console.log( + new Date().toISOString().replace(/[T.]/g, ' ').split(' ').slice(1,2).join(' '), + [...arguments] + .map(v => util.inspect(v, { + maxArrayLength: null, + breakLength: this.break, + colors: true, + compact: true, + depth: Infinity + })) + .join(' ') + ); +} + +const dgram = require("dgram"); +const SSDP_ADDRESS = "239.255.255.250"; +const SSDP_PORT = 1900; +const socket = dgram.createSocket("udp4"); + +socket.on("error", error => log({ error })); +socket.bind(1900, () => { + socket.addMembership(SSDP_ADDRESS); +}); + +const [local, name, host, code, serial] = args; +args = args.slice(3); + +console.log([ + `Broadcasting Bambu Printer Proxy`, + `Name: ${name}`, + `Host: ${host}`, + `Serial: ${serial}`, +].join('\n')); + +const ssdpMessage = ` +NOTIFY * HTTP/1.1 +HOST: ${SSDP_ADDRESS}:${SSDP_PORT} +Server: UPnP/1.0 +Location: ${local} +NT: urn:bambulab-com:device:3dprinter:1 +USN: ${serial} +Cache-Control: max-age=1800 +DevModel.bambu.com: C11 +DevName.bambu.com: ${name} +DevSignal.bambu.com: -66 +DevConnect.bambu.com: lan +DevBind.bambu.com: free +Devseclink.bambu.com: secure +DevVersion.bambu.com: 01.07.00.00 +DevCap.bambu.com: 1`.trim().split('\n').join("\r\n") + "\r\n\r\n"; + +console.log({ ssdpMessage }); + +// start broadcaster +setInterval(() => { + socket.send( + ssdpMessage, 0, + ssdpMessage.length, 2021, + SSDP_ADDRESS, + (err, data) => err && console.error("SSDP broadcast error:", err, data) + ); +}, 1000); + +// const { execSync } = require('child_process'); +const aedes = require('aedes')(); +const tls = require('tls'); +const fs = require('fs'); +const mqtt = require('mqtt'); +const path = require('path'); + +const CERT_DIR = './certs'; +const CERT_KEY_PATH = path.join(CERT_DIR, 'server-key.pem'); +const CERT_PATH = path.join(CERT_DIR, 'server-cert.pem'); +const CA_CERT_PATH = path.join(CERT_DIR, 'ca-cert.pem'); + +if (!fs.existsSync(CERT_DIR)) { + fs.mkdirSync(CERT_DIR); +} + +// Generate self-signed certificate if it does not exist (local cli testing only) +// if (!fs.existsSync(CERT_KEY_PATH) || !fs.existsSync(CERT_PATH)) { +// console.log('Generating self-signed certificate...'); +// execSync(`openssl req -x509 -newkey rsa:4096 -keyout ${CERT_KEY_PATH} -out ${CERT_PATH} -days 365 -nodes -subj "/CN=localhost"`); +// } +if (!fs.existsSync(CERT_KEY_PATH) || !fs.existsSync(CERT_PATH)) { + console.log('missing required key and cert'); + return; +} + +const options = { + key: fs.readFileSync(CERT_KEY_PATH), + cert: fs.readFileSync(CERT_PATH), + ca: fs.existsSync(CA_CERT_PATH) ? fs.readFileSync(CA_CERT_PATH) : undefined +}; + +const remoteMqttOptions = { + host, + port: 8883, + username: 'bblp', + password: code, + protocol: 'mqtts', + rejectUnauthorized: false +}; + +// Start local MQTTS broker +const server = tls.createServer(options, (socket) => { + log(`MQTTS server connection`, socket.remoteAddress, socket.remotePort); + aedes.handle(socket); +}); + +server.listen(8883, () => { + log('MQTTS server running on port 8883'); +}); + +// Connect to remote MQTTS broker +const remoteClient = mqtt.connect(remoteMqttOptions); + +remoteClient.on('connect', () => { + log('Connected to remote MQTTS broker'); +}); + +aedes.preConnect = (client, packet, callback) => { + let { id, version } = client; + let { cmd, username, password, clientId, protocolId } = packet; + log(`mqtts preconnect`, { + id, + version, + cmd, + username, + password: password ? password.toString() : undefined, + clientId, + protocolId + }); + callback(null, true); +}; + +aedes.authenticate = (client, username, password, callback) => { + log(`mqtts auth`, { username, password: password.toString() }); + const isValid = true; + callback(null, isValid); +}; + +// Proxy messages to remote broker +aedes.on('publish', (packet, client) => { + if (client && packet.topic !== 'aedes/keepalive') { + remoteClient.publish(packet.topic, packet.payload, { qos: packet.qos, retain: packet.retain }); + try { + let { topic, payload } = packet; + let json = JSON.parse(payload.toString().replace('\x00','')); + log('send', topic, json); + } catch (err) { + log({ err, packet, payload: packet.payload.toString() }); + } + } +}); + +// Subscribe to remote messages and forward to local clients +remoteClient.on('message', (topic, payload) => { + aedes.publish({ topic, payload }); + try { + let json = JSON.parse(payload.toString()); + log('recv', topic, json); + } catch (err) { + log({ topic, payload: payload.toString() }); + } +}); + +// Sync subscriptions +aedes.on('subscribe', (subscriptions, client) => { + subscriptions.forEach(sub => { + log({ subscribe: sub.topic }); + remoteClient.subscribe(sub.topic); + }); +}); + +aedes.on('unsubscribe', (subscriptions, client) => { + subscriptions.forEach(sub => { + log({ unsubscribe: sub.topic }); + remoteClient.unsubscribe(sub); + }); +}); diff --git a/mods/package.json b/mods/package.json index 4c555092..ad989a7f 100644 --- a/mods/package.json +++ b/mods/package.json @@ -1,4 +1,3 @@ - { "name": "grid-apps-mods", "version": "4.1.0", @@ -10,6 +9,7 @@ }, "dependencies": { "@gridspace/basic-ftp": "^5.0.5", + "aedes": "^0.51.3", "mqtt": "^5.10.3" } }