maziggy před 2 týdny
rodič
revize
59f7d736e3

+ 42 - 0
BACKERS.md

@@ -0,0 +1,42 @@
+# Bambuddy Backers & Sponsors
+
+Bambuddy is sustainable thanks to people who put their money where their use is. This page lists everyone who supports the project on [GitHub Sponsors](https://github.com/sponsors/maziggy) or [Ko-fi](https://ko-fi.com/maziggy).
+
+If you'd like to support Bambuddy:
+
+- **GitHub Sponsors** (recurring, 5 tiers from $5/mo to $500/mo) — https://github.com/sponsors/maziggy
+- **Ko-fi** (one-time or recurring) — https://ko-fi.com/maziggy
+
+If you sponsor and your name isn't here within 48h, please ping `maziggy` on Discord or open an Issue.
+
+---
+
+## Corporate Sponsors ($500/mo+)
+
+*None yet — be the first. Your logo on the bambuddy.cool homepage and press.html, plus co-marketing.*
+
+## Sustaining Sponsors ($150/mo+)
+
+*None yet.*
+
+## Patrons ($35/mo+)
+
+*None yet.*
+
+## Supporters ($15/mo+)
+
+*None yet.*
+
+## Backers ($5/mo+)
+
+*None yet.*
+
+---
+
+## One-time and historical supporters
+
+A general thank-you to everyone who's contributed via Ko-fi over the past months. Specific names get added on request — if you'd like to be listed, ping `maziggy`.
+
+---
+
+Thanks. — Martin

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
CHANGELOG.md


+ 1 - 1
README.md

@@ -94,7 +94,7 @@ You don't need to be a developer for the docs or moderator roles. If you enjoy w
 **Print from anywhere in the world** — Bambuddy's new Proxy Mode acts as a secure relay between your slicer and printer:
 
 - 🔒 **End-to-end TLS encryption** — FTP, file transfer, and camera are transparently proxied with the printer's real TLS certificate
-- 🛡️ **Optional Tailscale integration** — per-VP toggle + Docker socket mount surface the host's Tailscale IP on the VP card, so you know which `100.x.x.x` to paste into the slicer when you want a virtual printer reachable over your tailnet ([setup](https://wiki.bambuddy.cool/features/virtual-printer/)). Bambuddy's self-signed CA import is still required for the slicer side — the Bambu Studio / OrcaSlicer printer-MQTT trust path uses a bundled BBL CA, not the system trust store, so even a publicly-trusted cert wouldn't help. Tailscale's role is the private tunnel (reachability from anywhere, no port forwarding), not cert-import elimination.
+- 🛡️ **Optional Tailscale integration** — per-VP toggle + Docker socket mount surface the host's Tailscale IP on the VP card, so you know which `100.x.x.x` to paste into the slicer when you want a virtual printer reachable over your tailnet ([setup](https://wiki.bambuddy.cool/features/virtual-printer/)). Bambuddy's self-signed CA import is still required on the slicer side: Bambu Studio / OrcaSlicer validate printer TLS against a bundled BBL CA (not the system trust store), **and** their Add Printer dialog is IP-only (no hostname to match an LE cert against), so a publicly-trusted cert can't help on either dimension. Tailscale's role is the private tunnel (reachability from anywhere, no port forwarding), not cert-import elimination.
 - 🌍 **No cloud dependency** — Direct connection through your own Bambuddy server
 - 🔑 **Uses printer's access code** — No additional credentials needed
 - ⚡ **Full-speed printing** — Transparent TCP proxy, only MQTT is decrypted for IP rewriting

+ 3 - 2
backend/app/api/routes/cloud.py

@@ -42,6 +42,7 @@ from backend.app.schemas.cloud import (
     SlicerSettingUpdate,
 )
 from backend.app.services.bambu_cloud import (
+    _SLICER_API_VERSION,
     BambuCloudAuthError,
     BambuCloudError,
     BambuCloudService,
@@ -445,7 +446,7 @@ async def logout(
 
 @router.get("/settings", response_model=SlicerSettingsResponse)
 async def get_slicer_settings(
-    version: str = "02.04.00.70",
+    version: str = _SLICER_API_VERSION,
     db: AsyncSession = Depends(get_db),
     current_user: User | None = cloud_caller(Permission.CLOUD_AUTH),
 ):
@@ -543,7 +544,7 @@ async def get_setting_detail(
 
 @router.get("/filaments", response_model=list[SlicerSetting])
 async def get_filament_presets(
-    version: str = "02.04.00.70",
+    version: str = _SLICER_API_VERSION,
     db: AsyncSession = Depends(get_db),
     current_user: User | None = cloud_caller(Permission.FILAMENTS_READ),
 ):

+ 38 - 15
backend/app/services/bambu_cloud.py

@@ -14,6 +14,24 @@ logger = logging.getLogger(__name__)
 BAMBU_API_BASE = "https://api.bambulab.com"
 BAMBU_API_BASE_CN = "https://api.bambulab.cn"
 
+# Client identity sent to Bambu Lab's cloud services. We identify honestly as
+# Bambuddy — the URL in parens makes the source unambiguous so Bambu can
+# distinguish our traffic from impersonators. This is the opposite of what the
+# OrcaSlicer fork was called out for in the May 2026 Bambu Lab blog post
+# ("Setting the record straight on cloud access and community"): we do not
+# introduce ourselves as official Bambu Studio.
+_USER_AGENT = "Bambuddy/1.0 (+https://github.com/maziggy/bambuddy)"
+
+# The `/v1/iot-service/api/slicer/setting` endpoint requires a `version` query
+# parameter in the XX.YY.ZZ.WW format Bambu Studio releases use (without it the
+# API returns HTTP 400 "field 'version' is not set"; non-matching formats like
+# "bambuddy-1.0" return HTTP 422 "Invalid input parameters"). However, Bambu's
+# server accepts ANY value within that format — it doesn't validate against a
+# release manifest. We therefore use a neutral "1.0.0.0" placeholder that does
+# not impersonate any real Bambu Studio release. Our client identity is in the
+# User-Agent header.
+_SLICER_API_VERSION = "1.0.0.0"
+
 
 class BambuCloudError(Exception):
     """Base exception for Bambu Cloud errors."""
@@ -74,7 +92,7 @@ class BambuCloudService:
         """Get headers for authenticated requests."""
         headers = {
             "Content-Type": "application/json",
-            "User-Agent": "Bambuddy/1.0",
+            "User-Agent": _USER_AGENT,
         }
         if self.access_token:
             headers["Authorization"] = f"Bearer {self.access_token}"
@@ -174,24 +192,25 @@ class BambuCloudService:
             code: 6-digit TOTP code from authenticator app
         """
         try:
-            # TFA endpoint is on bambulab.com, NOT api.bambulab.com
-            # Requires browser-like headers to bypass Cloudflare
+            # TFA endpoint is on bambulab.com, NOT api.bambulab.com.
+            # We previously sent a Chrome User-Agent plus Origin/Referer headers
+            # under the assumption Cloudflare would block bot-identified
+            # requests. Verified 2026-05-12 via curl that the endpoint accepts
+            # honest "Bambuddy/X.Y.Z" identification cleanly (HTTP 400 with the
+            # expected application-level "Login failed" JSON, no Cloudflare
+            # interstitial). Browser-impersonation removed to stay clearly on
+            # the right side of Bambu Lab's "no falsified client identity" line.
             tfa_url = "https://bambulab.com/api/sign-in/tfa"
             if "bambulab.cn" in self.base_url:
                 tfa_url = "https://bambulab.cn/api/sign-in/tfa"
 
-            browser_headers = {
-                "Content-Type": "application/json",
-                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
-                "Accept": "application/json, text/plain, */*",
-                "Accept-Language": "en-US,en;q=0.9",
-                "Origin": "https://bambulab.com",
-                "Referer": "https://bambulab.com/",
-            }
-
             response = await self._client.post(
                 tfa_url,
-                headers=browser_headers,
+                headers={
+                    "Content-Type": "application/json",
+                    "User-Agent": _USER_AGENT,
+                    "Accept": "application/json",
+                },
                 json={
                     "tfaKey": tfa_key,
                     "tfaCode": code,
@@ -281,12 +300,16 @@ class BambuCloudService:
         except httpx.RequestError as e:
             raise BambuCloudError(f"Request failed: {e}")
 
-    async def get_slicer_settings(self, version: str = "02.04.00.70") -> dict:
+    async def get_slicer_settings(self, version: str = _SLICER_API_VERSION) -> dict:
         """
         Get all slicer settings (filament, printer, process presets).
 
         Args:
-            version: Slicer version string
+            version: Slicer version string. Bambu's API requires the XX.YY.ZZ.WW
+                format but does not validate against a release manifest — we
+                default to the neutral _SLICER_API_VERSION placeholder so we
+                never claim to be a specific Bambu Studio build. Callers should
+                normally use the default.
         """
         if not self.is_authenticated:
             raise BambuCloudAuthError("Not authenticated")

+ 5 - 1
backend/app/services/firmware_check.py

@@ -122,7 +122,11 @@ class FirmwareCheckService:
         self._client = httpx.AsyncClient(
             timeout=30.0,
             headers={
-                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+                # Identify honestly as Bambuddy when scraping the public Bambu
+                # Lab firmware wiki — verified 2026-05-12 that the wiki serves
+                # this UA identically to a Chrome UA (same HTML response shape).
+                # No browser impersonation needed for read-only public pages.
+                "User-Agent": "Bambuddy/1.0 (+https://github.com/maziggy/bambuddy)"
             },
         )
 

+ 9 - 4
backend/app/services/makerworld.py

@@ -43,11 +43,16 @@ MAKERWORLD_CDN_HOSTS = ("makerworld.bblmw.com", "public-cdn.bblmw.com")
 # Pr0zak/YASTL#52. The suffix check matches any regional S3 endpoint.
 _ALLOWED_DOWNLOAD_SUFFIXES = (".amazonaws.com",)
 
-# Browser-like headers. ``api.bambulab.com`` accepts minimal headers cleanly;
-# the Referer is kept so MakerWorld origin checks don't fail anywhere the
-# same client hits ``makerworld.com``.
+# Client identity sent to MakerWorld / api.bambulab.com. We identify honestly
+# as Bambuddy with a source URL so Bambu can distinguish our traffic from
+# impersonators — the opposite of what the OrcaSlicer fork was called out for
+# in the May 2026 Bambu Lab blog post on cloud access. Verified 2026-05-12 via
+# curl that MakerWorld treats this UA identically to a Firefox UA at the
+# Cloudflare edge (same response shape on /api/v1/design-service/* paths).
+# The Referer is kept because MakerWorld's CSRF / origin-check middleware uses
+# it on some endpoints — that's distinct from client impersonation.
 _CLIENT_HEADERS = {
-    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:147.0) Gecko/20100101 Firefox/147.0",
+    "User-Agent": "Bambuddy/1.0 (+https://github.com/maziggy/bambuddy)",
     "Accept": "text/html,application/json,*/*",
     "Accept-Language": "en-US,en;q=0.9",
     "Referer": "https://makerworld.com/",

+ 9 - 5
backend/app/services/virtual_printer/tailscale.py

@@ -6,11 +6,15 @@ they want to reach a VP over Tailscale.
 
 Historical note: this module previously provisioned Let's Encrypt certs via
 `tailscale cert` so the slicer would not need a manual CA import. That path
-was removed because BambuStudio's printer-MQTT trust path validates only
-against its bundled BBL CA (not the system trust store), so LE-signed certs
-are rejected regardless of hostname/IP. The self-signed CA flow (with one-
-time `bbl_ca.crt` import into the slicer) is the only viable trust mechanism;
-Tailscale's role is now strictly network reach.
+was removed because LE-signed certs can't help on two independent dimensions:
+(1) BambuStudio / OrcaSlicer printer-MQTT trust validates only against the
+bundled BBL CA, not the system trust store, so non-BBL chains are rejected
+at the issuer check; (2) both slicers' Add Printer dialog accepts only an
+IP address (not a hostname), so even if the trust store accepted the LE
+issuer, the cert's hostname (`*.<tailnet>.ts.net`) couldn't match the
+`100.x.x.x` connection target. The self-signed CA flow (one-time `bbl_ca.crt`
+import into the slicer) is the only viable trust mechanism; Tailscale's role
+is now strictly network reach.
 """
 
 import asyncio

+ 19 - 5
backend/tests/unit/services/test_bambu_cloud.py

@@ -218,8 +218,18 @@ class TestBambuCloudTOTPVerification:
             assert "Invalid response" in result["message"]
 
     @pytest.mark.asyncio
-    async def test_verify_totp_includes_browser_headers(self, cloud_service):
-        """TOTP verification should include browser-like headers to bypass Cloudflare."""
+    async def test_verify_totp_uses_honest_bambuddy_user_agent(self, cloud_service):
+        """TOTP verification identifies as Bambuddy, not as a browser.
+
+        The TOTP endpoint previously sent a Chrome User-Agent + Origin/Referer
+        headers under the assumption Cloudflare would block non-browser
+        identification. Verified 2026-05-12 that ``https://bambulab.com/api/sign-in/tfa``
+        accepts ``Bambuddy/X.Y.Z`` cleanly — the expected application-level
+        response comes back, no Cloudflare interstitial. Browser impersonation
+        was removed to stay clearly on the right side of Bambu Lab's
+        "no falsified client identity" line from the 2026-05-12 cloud-access
+        blog post.
+        """
         mock_response = MagicMock()
         mock_response.status_code = 200
         mock_response.text = '{"token": "test-token"}'
@@ -231,11 +241,15 @@ class TestBambuCloudTOTPVerification:
 
             await cloud_service.verify_totp("test-tfa-key", "123456")
 
-            # Check headers include User-Agent
             call_args = mock_post.call_args
             headers = call_args[1]["headers"]
-            assert "User-Agent" in headers
-            assert "Mozilla" in headers["User-Agent"]
+            assert headers["User-Agent"].startswith("Bambuddy/")
+            # Browser-impersonation strings must not creep back in
+            assert "Mozilla" not in headers["User-Agent"]
+            assert "Chrome" not in headers["User-Agent"]
+            # Origin / Referer headers were spoofing bambulab.com origin — gone
+            assert "Origin" not in headers
+            assert "Referer" not in headers
 
 
 class TestBambuCloudRegion:

+ 21 - 5
backend/tests/unit/services/test_makerworld.py

@@ -104,10 +104,21 @@ class TestGetDesign:
         assert url == "https://api.bambulab.com/v1/design-service/design/1"
 
     @pytest.mark.asyncio
-    async def test_sends_browser_like_headers(self, service):
-        """Post-refactor the client uses a minimal Firefox-ish header set.
-        The old ``x-bbl-*`` Bambu-app identification headers are gone —
-        ``api.bambulab.com`` accepts browser-like headers cleanly."""
+    async def test_sends_honest_bambuddy_user_agent(self, service):
+        """The client identifies honestly as Bambuddy, not as Firefox.
+
+        Earlier iterations of this code stripped ``x-bbl-*`` Bambu-app
+        identification headers but kept a Firefox User-Agent. Verified
+        2026-05-12 that MakerWorld treats ``Bambuddy/X.Y.Z`` identically to
+        a Firefox UA at the Cloudflare edge — same response shape on
+        ``/api/v1/design-service/*`` paths. Honest identification keeps us
+        clearly outside Bambu Lab's "no falsified client identity" line
+        from the 2026-05-12 cloud-access blog post.
+
+        Referer is still sent because MakerWorld's CSRF / origin-check
+        middleware uses it on some endpoints — that is functional, not
+        client-impersonation.
+        """
         resp = MagicMock()
         resp.status_code = 200
         resp.json.return_value = {"id": 1}
@@ -115,7 +126,12 @@ class TestGetDesign:
 
         await service.get_design(1)
         headers = service._client.get.call_args.kwargs["headers"]
-        assert "Firefox" in headers["User-Agent"]
+        assert headers["User-Agent"].startswith("Bambuddy/")
+        # Browser-impersonation strings must not creep back in
+        assert "Mozilla" not in headers["User-Agent"]
+        assert "Firefox" not in headers["User-Agent"]
+        assert "Chrome" not in headers["User-Agent"]
+        # Functional headers stay
         assert headers["Accept-Language"].startswith("en-US")
         assert headers["Referer"] == "https://makerworld.com/"
         assert "Accept" in headers

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů