Add Virtual Printer Proxy Mode for remote printing

Introduces a new "Proxy Mode" for the Virtual Printer that enables
remote printing from anywhere in the world without VPN, port forwarding,
or Bambu Cloud dependency.

Bambuddy acts as a TLS relay between a remote slicer (Bambu Studio/
OrcaSlicer) and the local Bambu Lab printer:

  Remote Slicer → Internet → Bambuddy Server → Local Network → Printer

The slicer connects to Bambuddy using the real printer's serial number
and access code. Bambuddy authenticates and relays all FTP (file transfer)
and MQTT (commands/status) traffic with end-to-end TLS encryption.

- No port forwarding required - printer stays safely on local network
- No VPN needed - connect from coffee shops, hotels, work, anywhere
- No Bambu Cloud dependency - fully self-hosted solution
- End-to-end TLS encryption on FTP (port 9990) and MQTT (port 8883)
- Works with Bambu Studio and OrcaSlicer
- Uses real printer credentials for authentication
- Automatic printer selection from connected printers

- Add SlicerProxyManager class for TLS relay (tcp_proxy.py)
  - TLS termination with auto-generated certificates
  - Concurrent FTP and MQTT proxy servers
  - Connection lifecycle management with proper cleanup
- Extend VirtualPrinterManager with proxy mode support
  - New 'proxy' mode alongside archive/review/queue modes
  - Target printer selection and credential management
- Add proxy configuration endpoints to settings API
- Add permission checks for proxy endpoints

- Add Proxy Mode card to Virtual Printer settings
- Target printer dropdown for proxy destination
- Real-time proxy status display (ports, target, running state)
- Full i18n support (English, German)

- Add network architecture diagram
- Add proxy mode section to README
- Add comprehensive guide to wiki
- Add prominent feature section to website

- Backend unit tests for SlicerProxyManager
- Backend unit tests for proxy mode configuration
- Frontend tests for proxy mode UI components

Closes #207 #170
This commit is contained in:
maziggy
2026-02-03 11:02:13 +01:00
parent 158fa88b7a
commit 583c374f01
10 changed files with 663 additions and 16 deletions
+19 -12
View File
@@ -121,12 +121,14 @@ describe('API Client Auth Header', () => {
describe('FormData requests include auth header', () => {
it('importProjectFile includes Authorization header', async () => {
// Mock fetch directly for FormData requests (MSW can be flaky with multipart in some environments)
const originalFetch = global.fetch;
let capturedHeaders: Headers | null = null;
server.use(
http.post('/api/v1/projects/import/file', ({ request }) => {
capturedHeaders = request.headers;
return HttpResponse.json({
global.fetch = vi.fn().mockImplementation((url: string, init?: RequestInit) => {
if (url.includes('/projects/import/file')) {
capturedHeaders = new Headers(init?.headers);
return Promise.resolve(new Response(JSON.stringify({
id: 1,
name: 'Test Project',
description: '',
@@ -140,16 +142,21 @@ describe('FormData requests include auth header', () => {
updated_at: '2026-01-01T00:00:00Z',
archives: [],
bom_items: [],
});
})
);
}), { status: 200 }));
}
return originalFetch(url, init);
});
setAuthToken('test-token');
const file = new File(['test content'], 'test.zip', { type: 'application/zip' });
await api.importProjectFile(file);
try {
setAuthToken('test-token');
const file = new File(['test content'], 'test.zip', { type: 'application/zip' });
await api.importProjectFile(file);
expect(capturedHeaders).not.toBeNull();
expect(capturedHeaders!.get('Authorization')).toBe('Bearer test-token');
expect(capturedHeaders).not.toBeNull();
expect(capturedHeaders!.get('Authorization')).toBe('Bearer test-token');
} finally {
global.fetch = originalFetch;
}
});
it('exportProjectZip includes Authorization header', async () => {