mirror of
https://github.com/ClusterM/open-bamboo-networking.git
synced 2026-10-19 01:29:46 -05:00
camera: drop client-side liveview.prepare and liveview signing
The cloud pushes liveview.prepare (with ttcode_enc) to the printer by itself in response to POST /v1/iot-service/api/user/ttcode, so the plugin never needs to publish it. Remove the signed prepare from remote_camera_url, rescue_cloud_liveview and its detached republish thread, the 84033543 frame filter, and revert the signing generalisation (signable_root_key, ttcode -> ttcode_enc) back to print-only. This also stops liveview frames from waiting up to 8 s on wait_for_app_cert.
This commit is contained in:
6 files changed
+50
-184
No files matched your search
@@ -417,11 +417,10 @@ public:
|
||||
// Studio then shows its normal "connection failed" state.
|
||||
std::string camera_url_for(const std::string& dev_id);
|
||||
// Remote cloud camera URL for bambu_network_get_camera_url when the LAN
|
||||
// route is unavailable. Fetches TUTK credentials via /v1/iot-service/api/user/ttcode,
|
||||
// proactively dispatches signed/encrypted prepare command to the printer,
|
||||
// and returns "bambu:///tutk?uid=...".
|
||||
// route is unavailable. Fetches TUTK credentials via /v1/iot-service/api/user/ttcode
|
||||
// and returns "bambu:///tutk?uid=...". The cloud itself pushes
|
||||
// liveview.prepare to the printer in response to that POST.
|
||||
std::string remote_camera_url(const std::string& dev_id);
|
||||
void rescue_cloud_liveview(const std::string& dev_id, const std::string& json);
|
||||
// Friendly name from the last SSDP packet for this printer IP, or "".
|
||||
std::string device_display_name_for_ip(const std::string& dev_ip) const;
|
||||
// Bearer + optional Studio certification headers for api.bambulab.com.
|
||||
@@ -470,8 +469,7 @@ private:
|
||||
const std::string& json);
|
||||
|
||||
// Records the printer's ipcam.tutk_server status ("enable" / "disable")
|
||||
// into tutk_server_ready_by_dev_ to prevent unnecessary liveview.prepare
|
||||
// commands that restart a running server.
|
||||
// into tutk_server_ready_by_dev_.
|
||||
void harvest_tutk_server_status(const std::string& dev_id,
|
||||
const std::string& json);
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@ typedef struct evp_pkey_st EVP_PKEY;
|
||||
|
||||
namespace obn::signing {
|
||||
|
||||
// Returns a signed envelope JSON for {"print":{...}} or {"liveview":{"command":"prepare",...}} payloads.
|
||||
// Returns a signed envelope JSON for {"print":{...}} payloads.
|
||||
// All other message types pass through unchanged.
|
||||
//
|
||||
// When `device_pub` is non-null, device-cert field encryption is applied to
|
||||
// the payload object *before* signing (so the signature covers the encrypted
|
||||
// form that goes on the wire): `url_enc` / `param_enc` / `ttcode_enc` are added
|
||||
// for the field the command carries (url for project_file, param for gcode_line,
|
||||
// ttcode for prepare).
|
||||
// the `print` object *before* signing (so the signature covers the encrypted
|
||||
// form that goes on the wire): `url_enc` / `param_enc` are added for the one
|
||||
// field the command carries (url for project_file, param for gcode_line).
|
||||
// `developer_mode` selects what happens to the cleartext field afterwards:
|
||||
// false (secured) — the cleartext is DROPPED (secured firmware reads only
|
||||
// *_enc and rejects a message carrying both — research/§10.3);
|
||||
@@ -29,10 +28,10 @@ std::string maybe_sign(const std::string& payload_json,
|
||||
bool developer_mode = false);
|
||||
|
||||
// True when maybe_sign() would actually sign `payload_json` — i.e. it carries a
|
||||
// top-level "print" or signable "liveview" object and a slicer key is configured.
|
||||
// Callers use this to gate a signed publish on the printer having installed the
|
||||
// app cert this session (a secured printer rejects a signature made before install
|
||||
// with 84033545 "need reset device pub key").
|
||||
// top-level "print" object and a slicer key is configured. Callers use this to
|
||||
// gate a signed publish on the printer having installed the app cert this
|
||||
// session (a secured printer rejects a signature made before install with
|
||||
// 84033545 "need reset device pub key").
|
||||
bool would_sign(const std::string& payload_json);
|
||||
|
||||
// Blockwise RSA-PKCS#1 v1.5 encryption of `plaintext` under `pub`, returned as
|
||||
|
||||
+4
-5
@@ -23,9 +23,9 @@ using obn::as_agent;
|
||||
// fetch video over RTSP(S) :322 instead of MJPEG :6000 on X1/P1S/P2S printers.
|
||||
//
|
||||
// When no LAN route is known (e.g. printer remote or off-LAN), we query the
|
||||
// Bambu cloud iot-service ttcode endpoint to mint a bambu:///tutk?... URL,
|
||||
// proactively dispatching the signed and encrypted liveview prepare command
|
||||
// so the printer starts its tutk_server.
|
||||
// Bambu cloud iot-service ttcode endpoint to mint a bambu:///tutk?... URL.
|
||||
// The cloud answers that POST by pushing liveview.prepare to the printer,
|
||||
// which starts its tutk_server.
|
||||
OBN_ABI int bambu_network_get_camera_url(void* agent,
|
||||
std::string dev_id,
|
||||
std::function<void(std::string)> callback)
|
||||
@@ -48,8 +48,7 @@ OBN_ABI int bambu_network_get_camera_url(void* agent,
|
||||
return BAMBU_NETWORK_SUCCESS;
|
||||
}
|
||||
|
||||
// When no LAN route is known, cloud URL minting and liveview.prepare
|
||||
// involves an HTTP POST and MQTT publish. Offload to a worker thread so
|
||||
// Cloud URL minting is an HTTP POST. Offload to a worker thread so
|
||||
// Studio's UI / MediaPlayCtrl thread returns immediately (research/08.11.1).
|
||||
std::thread([a, dev_id = std::move(dev_id), serial, callback = std::move(callback)]() {
|
||||
std::string cloud_url = a->remote_camera_url(dev_id);
|
||||
|
||||
-104
@@ -1240,81 +1240,6 @@ void Agent::maybe_install_app_cert(const std::string& dev_id)
|
||||
(void)request_app_cert_install(dev_id);
|
||||
}
|
||||
|
||||
void Agent::rescue_cloud_liveview(const std::string& dev_id,
|
||||
const std::string& json)
|
||||
{
|
||||
if (json.find("\"liveview\"") == std::string::npos) return;
|
||||
if (json.find("\"prepare\"") == std::string::npos) return;
|
||||
if (json.find("84033543") == std::string::npos) return;
|
||||
|
||||
auto root = obn::json::parse(json);
|
||||
if (!root) return;
|
||||
const obn::json::Value& lv_val = root->find("liveview");
|
||||
if (lv_val.kind() != obn::json::Value::Kind::Object) return;
|
||||
|
||||
obn::json::Object lv_obj = lv_val.as_object();
|
||||
|
||||
auto cmd_it = lv_obj.find("command");
|
||||
if (cmd_it == lv_obj.end() || !cmd_it->second.is_string()) return;
|
||||
if (cmd_it->second.as_string() != "prepare") return;
|
||||
|
||||
auto err_it = lv_obj.find("err_code");
|
||||
if (err_it == lv_obj.end()) return;
|
||||
{
|
||||
bool is_rejection = false;
|
||||
if (err_it->second.is_number()) {
|
||||
is_rejection = (err_it->second.as_int() == 84033543LL);
|
||||
}
|
||||
if (!is_rejection) return;
|
||||
}
|
||||
|
||||
std::string ttcode;
|
||||
auto tt_it = lv_obj.find("ttcode");
|
||||
if (tt_it != lv_obj.end() && tt_it->second.is_string())
|
||||
ttcode = tt_it->second.as_string();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
auto it_ready = tutk_server_ready_by_dev_.find(dev_id);
|
||||
if (it_ready != tutk_server_ready_by_dev_.end() && it_ready->second) {
|
||||
OBN_INFO("rescue_cloud_liveview dev=%s: tutk_server already running (enable) - skipping prepare to prevent server restart",
|
||||
dev_id.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
static std::map<std::string, std::chrono::steady_clock::time_point> last_rescue;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
auto it = last_rescue.find(dev_id);
|
||||
if (it != last_rescue.end() && now - it->second < std::chrono::seconds(2)) {
|
||||
OBN_DEBUG("rescue_cloud_liveview dev=%s: rescue cooldown active (2s), skip duplicate",
|
||||
dev_id.c_str());
|
||||
return;
|
||||
}
|
||||
last_rescue[dev_id] = now;
|
||||
}
|
||||
|
||||
OBN_INFO("rescue_cloud_liveview dev=%s ttcode=%s: intercepting unsigned rejection, signing and republishing",
|
||||
dev_id.c_str(), ttcode.c_str());
|
||||
|
||||
lv_obj.erase("err_code");
|
||||
lv_obj["sequence_id"] = obn::json::Value(obn::next_mqtt_seq_id());
|
||||
|
||||
obn::json::Object new_root;
|
||||
new_root["liveview"] = obn::json::Value(std::move(lv_obj));
|
||||
const std::string req_json = obn::json::Value(std::move(new_root)).dump();
|
||||
|
||||
std::string dev_id_copy = dev_id;
|
||||
std::thread([this, dev_id_copy, req_json]() mutable {
|
||||
int rc = send_message(dev_id_copy, req_json, /*qos=*/0);
|
||||
if (rc == BAMBU_NETWORK_SUCCESS) {
|
||||
OBN_INFO("rescue_cloud_liveview dev=%s: signed liveview prepare dispatched OK",
|
||||
dev_id_copy.c_str());
|
||||
} else {
|
||||
OBN_WARN("rescue_cloud_liveview dev=%s: send_message failed rc=%d",
|
||||
dev_id_copy.c_str(), rc);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
int Agent::send_message_to_printer(const std::string& dev_id,
|
||||
const std::string& json_str,
|
||||
int qos)
|
||||
@@ -1587,27 +1512,6 @@ std::string Agent::remote_camera_url(const std::string& dev_id)
|
||||
|
||||
if (tt_resp.region.empty()) tt_resp.region = "us";
|
||||
|
||||
// Proactively send signed and encrypted prepare command so printer starts tutk_server
|
||||
{
|
||||
obn::json::Object lv_obj;
|
||||
lv_obj["command"] = obn::json::Value(std::string("prepare"));
|
||||
lv_obj["sequence_id"] = obn::json::Value(obn::next_mqtt_seq_id());
|
||||
lv_obj["ttcode"] = obn::json::Value(tt_resp.uid);
|
||||
lv_obj["authkey"] = obn::json::Value(tt_resp.authkey);
|
||||
lv_obj["passwd"] = obn::json::Value(tt_resp.passwd);
|
||||
lv_obj["region"] = obn::json::Value(tt_resp.region);
|
||||
|
||||
obn::json::Object new_root;
|
||||
new_root["liveview"] = obn::json::Value(std::move(lv_obj));
|
||||
const std::string req_json = obn::json::Value(std::move(new_root)).dump();
|
||||
|
||||
OBN_INFO("camera_url(remote): dispatching liveview prepare for dev=%s uid=%s",
|
||||
serial.c_str(), tt_resp.uid.c_str());
|
||||
int rc = send_message(serial, req_json, /*qos=*/0);
|
||||
OBN_INFO("camera_url(remote): liveview prepare dev=%s rc=%d",
|
||||
serial.c_str(), rc);
|
||||
}
|
||||
|
||||
std::string turl = obn::camera::build_tutk_url(
|
||||
tt_resp.uid, tt_resp.authkey, tt_resp.passwd, tt_resp.region);
|
||||
OBN_INFO("camera_url(remote): built tutk url for dev=%s uid=%.20s region=%s",
|
||||
@@ -1655,7 +1559,6 @@ void Agent::notify_local_message(const std::string& dev_id, const std::string& j
|
||||
harvest_developer_mode(dev_id, json);
|
||||
harvest_media_caps(dev_id, json);
|
||||
harvest_tutk_server_status(dev_id, json);
|
||||
rescue_cloud_liveview(dev_id, json);
|
||||
|
||||
// LAN telemetry is authoritative: stamp the report and, on the first one,
|
||||
// defer-close the cloud report subscription for this device.
|
||||
@@ -2813,13 +2716,6 @@ int Agent::connect_cloud()
|
||||
harvest_developer_mode(dev_id, json);
|
||||
harvest_media_caps(dev_id, json);
|
||||
harvest_tutk_server_status(dev_id, json);
|
||||
rescue_cloud_liveview(dev_id, json);
|
||||
|
||||
// Drop rejected unsigned cloud liveview frames from notifying Studio UI
|
||||
if (json.find("\"liveview\"") != std::string::npos &&
|
||||
json.find("84033543") != std::string::npos) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirror Bambu's plugin: the FIRST cloud report we receive
|
||||
// for a device kicks off an on_printer_connected("tunnel/<id>")
|
||||
|
||||
+27
-37
@@ -235,31 +235,24 @@ std::string rsa_pkcs1_sign_raw_b64(EVP_PKEY* pkey,
|
||||
return base64_encode(sig.data(), siglen);
|
||||
}
|
||||
|
||||
// Returns "print", "liveview" (only for prepare commands), or empty if neither.
|
||||
std::string signable_root_key(const std::string& payload) noexcept
|
||||
// Returns true if the payload's first JSON key is "print".
|
||||
bool is_print_payload(const std::string& payload) noexcept
|
||||
{
|
||||
const char* p = payload.data();
|
||||
const char* end = p + payload.size();
|
||||
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) ++p;
|
||||
if (p >= end || *p != '{') return {};
|
||||
if (p >= end || *p != '{') return false;
|
||||
++p;
|
||||
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) ++p;
|
||||
if (p >= end || *p != '"') return {};
|
||||
++p;
|
||||
const char* kstart = p;
|
||||
while (p < end && *p != '"') ++p;
|
||||
if (p >= end) return {};
|
||||
std::string key(kstart, p - kstart);
|
||||
if (key == "print") return key;
|
||||
if (key == "liveview") {
|
||||
if (payload.find("\"prepare\"") != std::string::npos) return key;
|
||||
}
|
||||
return {};
|
||||
const char kKey[] = "\"print\"";
|
||||
if (p + 7 > end) return false;
|
||||
for (int i = 0; i < 7; ++i)
|
||||
if (p[i] != kKey[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The one field each command carries as device-cert ciphertext
|
||||
// (research/10.03): project_file's model url, gcode_line's raw G-code,
|
||||
// prepare's liveview ttcode.
|
||||
// (research/10.03): project_file's model url, gcode_line's raw G-code.
|
||||
// project_file's `param` is the plate path the firmware reads in cleartext;
|
||||
// encrypting (and dropping) it makes the printer fail with 0500-4003
|
||||
// (cannot parse file).
|
||||
@@ -270,7 +263,6 @@ const char* encrypted_field_for(const obn::json::Object& obj)
|
||||
const std::string c = cmd->second.as_string();
|
||||
if (c == "project_file") return "url";
|
||||
if (c == "gcode_line") return "param";
|
||||
if (c == "prepare") return "ttcode";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -310,18 +302,16 @@ void encrypt_print_fields(obn::json::Object& obj, EVP_PKEY* device_pub,
|
||||
obj.erase(it);
|
||||
}
|
||||
|
||||
// Builds the command dump ({...sorted keys...}) after optional field encryption.
|
||||
// Builds the print dump ({...sorted keys...}) after optional field encryption.
|
||||
// Uses json_lite, whose Object type is std::map, so dump() already sorts keys.
|
||||
std::string build_command_dump(const std::string& root_key,
|
||||
const std::string& payload,
|
||||
EVP_PKEY* device_pub,
|
||||
bool developer_mode)
|
||||
std::string build_print_dump(const std::string& payload, EVP_PKEY* device_pub,
|
||||
bool developer_mode)
|
||||
{
|
||||
auto root = obn::json::parse(payload);
|
||||
if (!root) return {};
|
||||
const obn::json::Value& cmd = root->find(root_key.c_str());
|
||||
if (cmd.kind() != obn::json::Value::Kind::Object) return {};
|
||||
obn::json::Object obj = cmd.as_object(); // copy for mutation
|
||||
const obn::json::Value& print = root->find("print");
|
||||
if (print.kind() != obn::json::Value::Kind::Object) return {};
|
||||
obn::json::Object obj = print.as_object(); // copy for mutation
|
||||
encrypt_print_fields(obj, device_pub, developer_mode);
|
||||
return obn::json::Value(std::move(obj)).dump();
|
||||
}
|
||||
@@ -342,8 +332,7 @@ static std::string json_str_escape(const std::string& s)
|
||||
// Builds the complete signed envelope JSON string.
|
||||
std::string build_envelope(const std::string& to_sign,
|
||||
const std::string& sig_b64,
|
||||
const std::string& root_key,
|
||||
const std::string& dump)
|
||||
const std::string& print_dump)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(to_sign.size() + sig_b64.size() + 200);
|
||||
@@ -357,8 +346,8 @@ std::string build_envelope(const std::string& to_sign,
|
||||
out += json_str_escape(sig_b64);
|
||||
out += "\",\"sign_ver\":\"";
|
||||
out += kSignVer;
|
||||
out += "\"},\"" + root_key + "\":";
|
||||
out += dump;
|
||||
out += "\"},\"print\":";
|
||||
out += print_dump;
|
||||
out += '}';
|
||||
return out;
|
||||
}
|
||||
@@ -367,7 +356,7 @@ std::string build_envelope(const std::string& to_sign,
|
||||
|
||||
bool would_sign(const std::string& payload_json)
|
||||
{
|
||||
return !signable_root_key(payload_json).empty() && slicer_pkey() != nullptr;
|
||||
return is_print_payload(payload_json) && slicer_pkey() != nullptr;
|
||||
}
|
||||
|
||||
bool slicer_signing_key_present()
|
||||
@@ -378,23 +367,24 @@ bool slicer_signing_key_present()
|
||||
std::string maybe_sign(const std::string& payload_json, EVP_PKEY* device_pub,
|
||||
bool developer_mode)
|
||||
{
|
||||
const std::string root_key = signable_root_key(payload_json);
|
||||
if (root_key.empty()) return payload_json;
|
||||
if (!is_print_payload(payload_json)) return payload_json;
|
||||
|
||||
EVP_PKEY* pkey = slicer_pkey();
|
||||
if (!pkey) return payload_json;
|
||||
|
||||
const std::string dump =
|
||||
build_command_dump(root_key, payload_json, device_pub, developer_mode);
|
||||
if (dump.empty()) return payload_json; // malformed; pass through
|
||||
// Encrypt url/param into url_enc/param_enc (when a device key is given)
|
||||
// BEFORE signing, so the signature covers exactly what goes on the wire.
|
||||
const std::string print_dump =
|
||||
build_print_dump(payload_json, device_pub, developer_mode);
|
||||
if (print_dump.empty()) return payload_json; // malformed; pass through
|
||||
|
||||
const std::string to_sign = "{\"" + root_key + "\":" + dump + "}";
|
||||
const std::string to_sign = std::string("{\"print\":") + print_dump + '}';
|
||||
|
||||
const std::string sig_b64 = rsa_sha256_sign_b64(
|
||||
pkey,
|
||||
reinterpret_cast<const unsigned char*>(to_sign.data()), to_sign.size());
|
||||
|
||||
return build_envelope(to_sign, sig_b64, root_key, dump);
|
||||
return build_envelope(to_sign, sig_b64, print_dump);
|
||||
}
|
||||
|
||||
std::string sign_bytes(const std::string& data)
|
||||
|
||||
+7
-23
@@ -493,30 +493,14 @@ static int test_blockwise_multiblock_roundtrip()
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_liveview_prepare_ttcode_encrypted()
|
||||
static int test_liveview_prepare_passthrough()
|
||||
{
|
||||
const std::string ttcode = "VGC47JEVNKCHC9RZ111A";
|
||||
// The cloud pushes liveview.prepare itself after POST /user/ttcode; the
|
||||
// client never signs or field-encrypts liveview frames.
|
||||
const std::string payload =
|
||||
R"({"liveview":{"authkey":"c0c597fc","command":"prepare","passwd":"a71c21","region":"us","sequence_id":"21143","ttcode":")" +
|
||||
ttcode + R"("}})";
|
||||
|
||||
// Secured printer (developer_mode = false): ttcode_enc added, cleartext ttcode dropped.
|
||||
const std::string env_sec = obn::signing::maybe_sign(payload, g_test_key, /*developer_mode=*/false);
|
||||
auto val_sec = obn::json::parse(env_sec);
|
||||
CHECK(val_sec);
|
||||
CHECK(val_sec->find("header.sign_string").kind() == obn::json::Value::Kind::String);
|
||||
CHECK(val_sec->find("liveview.ttcode").kind() == obn::json::Value::Kind::Null);
|
||||
CHECK(val_sec->find("liveview.ttcode_enc").kind() == obn::json::Value::Kind::String);
|
||||
CHECK(rsa_decrypt_blocks_b64(val_sec->find("liveview.ttcode_enc").as_string()) == ttcode);
|
||||
CHECK(val_sec->find("liveview.authkey").as_string() == "c0c597fc");
|
||||
CHECK(val_sec->find("liveview.passwd").as_string() == "a71c21");
|
||||
|
||||
// Developer Mode printer (developer_mode = true): ttcode_enc added, cleartext ttcode kept.
|
||||
const std::string env_dev = obn::signing::maybe_sign(payload, g_test_key, /*developer_mode=*/true);
|
||||
auto val_dev = obn::json::parse(env_dev);
|
||||
CHECK(val_dev);
|
||||
CHECK(val_dev->find("liveview.ttcode").as_string() == ttcode);
|
||||
CHECK(val_dev->find("liveview.ttcode_enc").kind() == obn::json::Value::Kind::String);
|
||||
R"({"liveview":{"command":"prepare","sequence_id":"0","ttcode":"TW3UK1278NNMSZD1111A"}})";
|
||||
CHECK(!obn::signing::would_sign(payload));
|
||||
CHECK(obn::signing::maybe_sign(payload, g_test_key, false) == payload);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -576,7 +560,7 @@ int main()
|
||||
if (test_no_device_key_leaves_cleartext() != 0) rc = 1;
|
||||
if (test_param_enc_idempotent() != 0) rc = 1;
|
||||
if (test_blockwise_multiblock_roundtrip() != 0) rc = 1;
|
||||
if (test_liveview_prepare_ttcode_encrypted() != 0) rc = 1;
|
||||
if (test_liveview_prepare_passthrough() != 0) rc = 1;
|
||||
|
||||
if (rc == 0) std::cout << "signing_test: ok\n";
|
||||
|
||||
|
||||
Reference in new issue
Block a user