Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/console/test/computer-directory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,10 @@ test("untrusted payloads are bounded and cannot substitute signatures, audiences
headers: { "Content-Type": "text/plain" },
body: "{}",
});
expect(wrongType.status).toBe(415);
await wrongType.body?.cancel();
expect({ status: wrongType.status, body: await wrongType.text() }).toEqual({
status: 415,
body: expect.stringContaining('"code":"json_required"'),
});
expect((await request(f, "PUT", "/records/" + host.id, good)).status).toBe(200);
const read = await lookup(f, host, phone);
expect(read.status).toBe(200);
Expand Down
21 changes: 18 additions & 3 deletions daemon/internal/surface/mac.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,26 @@ func macConnectionError(ctx context.Context, err error) error {
return ctx.Err()
}
var network net.Error
if errors.As(err, &network) && network.Timeout() {
if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &network) && network.Timeout()) {
return problem("timeout", "The Mac's screen-sharing service did not respond. Try again.")
}
// Never propagate an authentication library's raw response into logs or chats.
return problem("desktop_authentication", "Mac screen sharing could not authenticate. Run mindwire desktop enable on your Mac to check access and update its login.")
var specific *Error
if errors.As(err, &specific) {
return specific
}
var handshake *rfbHandshakeFailure
if errors.As(err, &handshake) {
err = handshake.cause
}
// go-vnc has no typed rejection error. Match its fixed prefix only; never
// propagate the server-supplied reason or turn a broken socket into bad login.
if strings.HasPrefix(err.Error(), "SecurityResult handshake failed:") {
return problem("desktop_authentication", "The Mac did not accept this login. Run mindwire desktop enable on your Mac to retry its password and check Screen Sharing access.")
}
if strings.HasPrefix(err.Error(), "Security handshake failed; no suitable auth schemes found;") {
return problem("desktop_auth_protocol", "Allow your Mac account in Screen Sharing settings, then run mindwire desktop enable again.")
}
return problem("unavailable", "The Mac's screen-sharing connection ended. Check Screen Sharing and try again.")
}

func (p *MacDesktop) Capture(ctx context.Context) (image.Image, Geometry, error) {
Expand Down
9 changes: 8 additions & 1 deletion daemon/internal/surface/mac_native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ func TestNativeMacDesktopFixture(t *testing.T) {
t.Skip("opt in with the iOS Mac desktop integration test")
}
s, store, frames, _ := macTestService(t)
live := os.Getenv("MINDWIRE_MAC_PERFORMANCE_STATE_DIR") != ""
if live {
// The read-only performance test uses existing, explicitly enabled access.
// Keep the real login only in memory, never in this fixture's state files.
s.configureProvider(performanceMac(t))
}
frames.resize = true
// A six-MiB frame crosses the native forwarder's write high-water mark.
// Tiny RFB fixtures miss stalls when NIOSSH reads resume after backpressure.
frames.largeFrame = true
frames.tiledFrame = true
s.SetApproval(func(context.Context, Actor, string) error { return nil })
directory := t.TempDir()
const token = "synthetic-mac-desktop-fixture"
Expand Down Expand Up @@ -107,7 +114,7 @@ func TestNativeMacDesktopFixture(t *testing.T) {
}
invitations = append(invitations, inv)
}
data, _ := json.Marshal(map[string]any{"invitations": invitations})
data, _ := json.Marshal(map[string]any{"invitations": invitations, "live": live})
if err = os.WriteFile(file, data, 0600); err != nil {
t.Fatal(err)
}
Expand Down
140 changes: 140 additions & 0 deletions daemon/internal/surface/mac_performance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package surface

import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"testing"
"time"

"github.com/oblien/mindwire/daemon/internal/session"
)

// Opt-in, read-only measurement against the owner's already-enabled Mac.
// No input, screenshots, settings changes or credentials are emitted or saved.
func TestMacDesktopReadOnlyPerformance(t *testing.T) {
p := performanceMac(t)
defer p.Close()
ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second)
defer cancel()
measureMacDesktop(t, ctx, p)
}

func performanceMac(t *testing.T) *MacDesktop {
t.Helper()
directory := os.Getenv("MINDWIRE_MAC_PERFORMANCE_STATE_DIR")
if directory == "" {
t.Skip("requires an explicitly selected, enabled Mac desktop")
}
store, err := session.Open(filepath.Join(directory, "agent-state.json"))
if err != nil {
t.Fatal("could not read local desktop setup")
}
var saved savedLocalDesktop
if json.Unmarshal([]byte(store.Get(localDesktopKey)), &saved) != nil || !saved.Enabled {
t.Fatal("enable desktop access locally before measuring")
}
p, err := newMacDesktop(saved.LocalDesktopSettings)
if err != nil {
t.Fatal(err)
}
return p
}

func measureMacDesktop(t *testing.T, ctx context.Context, p *MacDesktop) {
geometry, err := p.Connect(ctx)
if err != nil {
t.Fatal(err)
}
var checks []time.Duration
for range 10 {
start := time.Now()
if _, err := p.Connect(ctx); err != nil {
t.Fatal(err)
}
checks = append(checks, time.Since(start))
}
sort.Slice(checks, func(i, j int) bool { return checks[i] < checks[j] })
t.Logf("display=%dx%d geometry-check median=%s max=%s", geometry.Width, geometry.Height, checks[len(checks)/2], checks[len(checks)-1])

// Match the compressed encoding used by Apple Screen Sharing without
// retaining/decoding pixels. This isolates server/transport cost from UI work.
conn, geometry, err := p.openViewer(ctx)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(25 * time.Second))
var encodings bytes.Buffer
encodings.Write([]byte{2, 0, 0, 4})
preferred := int32(6)
if value, err := strconv.Atoi(os.Getenv("MINDWIRE_MAC_PERFORMANCE_ENCODING")); err == nil && value == 16 {
preferred = 16
}
for _, encoding := range []int32{preferred, 0, -224, -223} {
_ = binary.Write(&encodings, binary.BigEndian, encoding)
}
if _, err := conn.Write(encodings.Bytes()); err != nil {
t.Fatal(err)
}
for frame := range 3 {
start := time.Now()
request := []byte{3, 0, 0, 0, 0, 0, 0, 0, 0, 0}
binary.BigEndian.PutUint16(request[6:8], uint16(geometry.Width))
binary.BigEndian.PutUint16(request[8:10], uint16(geometry.Height))
if _, err := conn.Write(request); err != nil {
t.Fatal(err)
}
var header [4]byte
if _, err := io.ReadFull(conn, header[:]); err != nil || header[0] != 0 {
t.Fatal("expected a framebuffer update", err)
}
count := int(binary.BigEndian.Uint16(header[2:]))
wireBytes, rectangles := int64(4), 0
used := map[int32]int{}
for range count {
var rect [12]byte
if _, err := io.ReadFull(conn, rect[:]); err != nil {
t.Fatal(err)
}
wireBytes += 12
encoding := int32(binary.BigEndian.Uint32(rect[8:]))
if encoding == -224 {
break
}
if encoding == -223 {
continue
}
var size int64
switch encoding {
case 6, 16:
var length [4]byte
if _, err := io.ReadFull(conn, length[:]); err != nil {
t.Fatal(err)
}
wireBytes += 4
size = int64(binary.BigEndian.Uint32(length[:]))
case 0:
size = int64(binary.BigEndian.Uint16(rect[4:])) * int64(binary.BigEndian.Uint16(rect[6:])) * 4
default:
t.Fatalf("unexpected encoding %d", encoding)
}
if size > 64<<20 {
t.Fatal("frame exceeds measurement limit")
}
if _, err := io.CopyN(io.Discard, conn, size); err != nil {
t.Fatal(err)
}
wireBytes += size
rectangles++
used[encoding]++
}
t.Logf("frame=%d receive=%s rectangles=%d wireBytes=%d encodings=%v", frame, time.Since(start), rectangles, wireBytes, used)
}
}
31 changes: 29 additions & 2 deletions daemon/internal/surface/mac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ func TestMacDesktopOptInAuthenticationAndIdempotentSetup(t *testing.T) {
if _, err := s.Open(t.Context(), Actor{Kind: "user"}, OpenRequest{RequestID: "before", Mode: "view"}); err == nil {
t.Fatal("desktop opened before opt-in")
}
if _, err := s.ConfigureLocalDesktop(t.Context(), LocalDesktopSettings{Enabled: true, Username: "owner", Password: "wrong"}, store); err == nil {
t.Fatal("bad Mac login was accepted")
if _, err := s.ConfigureLocalDesktop(t.Context(), LocalDesktopSettings{Enabled: true, Username: "owner", Password: "wrong"}, store); asError(err).Code != "desktop_authentication" {
t.Fatal("bad Mac login did not return a retryable authentication error", err)
}
if store.Get(localDesktopKey) != "" {
t.Fatal("failed authentication persisted settings")
Expand All @@ -295,6 +295,13 @@ func TestMacDesktopOptInAuthenticationAndIdempotentSetup(t *testing.T) {
if accepted.Load() != count || s.Snapshot().Controller == nil || s.Snapshot().Controller.SessionID != viewer.ID {
t.Fatal("identical setup restarted authentication or lost control")
}
saved := store.Get(localDesktopKey)
if _, err := s.ConfigureLocalDesktop(t.Context(), LocalDesktopSettings{Enabled: true, Username: "owner", Password: "wrong again"}, store); asError(err).Code != "desktop_authentication" {
t.Fatal("failed reconfiguration did not preserve the login error", err)
}
if saved != store.Get(localDesktopKey) || s.Snapshot().Controller == nil || s.Snapshot().Controller.SessionID != viewer.ID {
t.Fatal("rejected password changed working credentials or dropped control")
}
capture, err := s.Capture(t.Context(), viewer.ID, Actor{Kind: "user"})
if err != nil || capture.Geometry.Width != 2 {
t.Fatal("Mac capture", err)
Expand Down Expand Up @@ -335,6 +342,26 @@ func TestMacDesktopOptInAuthenticationAndIdempotentSetup(t *testing.T) {
}
}

func TestMacConnectionErrorsDistinguishRejectedLoginFromBrokenConnection(t *testing.T) {
for _, test := range []struct {
err error
code string
}{
{errors.New("SecurityResult handshake failed: private upstream response"), "desktop_authentication"},
{&rfbHandshakeFailure{cause: errors.New("SecurityResult handshake failed: private upstream response")}, "desktop_authentication"},
{&rfbHandshakeFailure{cause: context.DeadlineExceeded}, "timeout"},
{&rfbHandshakeFailure{cause: io.EOF}, "unavailable"},
{io.ErrUnexpectedEOF, "unavailable"},
{problem("desktop_auth_protocol", "Unsupported Screen Sharing authentication."), "desktop_auth_protocol"},
{errors.New("Security handshake failed; no suitable auth schemes found; server supports: private upstream response"), "desktop_auth_protocol"},
} {
got := asError(macConnectionError(t.Context(), test.err))
if got.Code != test.code || strings.Contains(got.Message, "private upstream") {
t.Fatalf("wrong or unsanitized desktop failure: %+v", got)
}
}
}

func TestMacViewerSharesNativeFramesButCannotSendInput(t *testing.T) {
s, store, fixture, _ := macTestService(t)
enableTestMac(t, s, store)
Expand Down
5 changes: 4 additions & 1 deletion daemon/internal/surface/rfb.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ func newRFBWithAuth(ctx context.Context, conn net.Conn, auth []vnc.ClientAuth) (
client, err := connectDesktopRFB(ctx, conn, cfg)
if err != nil {
conn.Close()
return nil, problem("rfb_handshake", "The desktop VNC connection could not be opened.")
if ctx.Err() != nil {
err = ctx.Err()
}
return nil, &rfbHandshakeFailure{cause: err}
}
width, height := int(client.FramebufferWidth()), int(client.FramebufferHeight())
if width < 1 || height < 1 || width*height > 16<<20 {
Expand Down
7 changes: 7 additions & 0 deletions daemon/internal/surface/rfb_handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import (
vnc "github.com/kward/go-vnc"
)

// Keep the underlying failure available for classification without exposing an
// RFB server's raw authentication response in logs or API messages.
type rfbHandshakeFailure struct{ cause error }

func (*rfbHandshakeFailure) Error() string { return "The desktop VNC connection could not be opened." }
func (e *rfbHandshakeFailure) Unwrap() error { return e.cause }

// The pinned go-vnc revision writes the RGB maxima in SetPixelFormat using
// little-endian rather than RFB's network byte order. Connect sends this message
// itself: correcting the format afterwards is too late for strict servers such
Expand Down
23 changes: 23 additions & 0 deletions daemon/internal/surface/rfb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type rfbFixture struct {
inputs []wireInput
resize bool
largeFrame bool
tiledFrame bool
debug bool
frameRequests int
authentication func(io.ReadWriteCloser) bool
Expand Down Expand Up @@ -130,6 +131,28 @@ func (f *rfbFixture) serve(conn io.ReadWriteCloser) {
}
}
var frame bytes.Buffer
if encoding == 0 && f.tiledFrame {
// Apple/ZRLE-style batches expose hundreds of dirty regions to the
// native viewer. Full-screen copies per region used to stall it.
const tile = 64
frame.Write([]byte{0, 0})
_ = binary.Write(&frame, binary.BigEndian, uint16(((int(w)+tile-1)/tile)*((int(h)+tile-1)/tile)))
for y := 0; y < int(h); y += tile {
for x := 0; x < int(w); x += tile {
tw, th := min(tile, int(w)-x), min(tile, int(h)-y)
for _, value := range []uint16{uint16(x), uint16(y), uint16(tw), uint16(th)} {
_ = binary.Write(&frame, binary.BigEndian, value)
}
_ = binary.Write(&frame, binary.BigEndian, int32(0))
frame.Write(bytes.Repeat([]byte{40, 30, 200, 0}, tw*th))
}
}
sentPixels = true
if _, err := conn.Write(frame.Bytes()); err != nil {
return
}
continue
}
frame.Write([]byte{0, 0, 0, 1})
for _, v := range []uint16{0, 0, w, h} {
_ = binary.Write(&frame, binary.BigEndian, v)
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ mindwire desktop disable # Close desktop access and forget its login
```

The phone uses its existing paired SSH tunnel and never receives the Mac password.
If macOS rejects the login, the CLI lets you retry the password or check Screen
Sharing permissions. Canceling or entering a wrong password keeps the existing
desktop setup and active connections unchanged.
Opening a workspace or checking status does not enable screen access. See the
[desktop contract](../../daemon/DESKTOP.md#personal-mac-desktop) for platform support,
permissions and the shared input protocol. Existing installations need a release
Expand Down
31 changes: 28 additions & 3 deletions packages/sdk/src/computer/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
import { readFile } from "node:fs/promises";
import * as path from "node:path";
import type { Mindwire } from "../client.js";
import { ApiError } from "../errors.js";
import type { LocalDesktopInfo } from "../surfaces.js";
import { terminalSetupPrompt, type SetupPrompt } from "./setup-prompt.js";
import { terminalText } from "./terminal-ui.js";
Expand Down Expand Up @@ -67,9 +68,33 @@ export async function manageDesktop(options: {
}
info = await client.surfaces.localStatus();
}
const password = await prompt.secret(`Mac password for ${terminalText(info.username)} (hidden): `);
if (!password || Buffer.byteLength(password) > 63 || password.includes("\0")) throw new Error("Enter your Mac password (up to 63 UTF-8 bytes). Desktop settings were not changed.");
await client.surfaces.configureLocal({ enabled: true, username: info.username, password }, localToken);
while (true) {
const password = await prompt.secret(`Mac password for ${terminalText(info.username)} (hidden): `);
let retryMessage: string;
if (!password || Buffer.byteLength(password) > 63 || password.includes("\0")) {
retryMessage = "Enter your Mac login password (up to 63 UTF-8 bytes).";
} else {
try {
await client.surfaces.configureLocal({ enabled: true, username: info.username, password }, localToken);
break;
} catch (error) {
if (!(error instanceof ApiError) || !error.body || typeof error.body !== "object" ||
!("code" in error.body) || error.body.code !== "desktop_authentication") throw error;
retryMessage = `The Mac didn’t accept that login. Check the password for ${terminalText(info.username)} and try again.\nUse your Mac login password, not your Apple Account password. If it’s correct, check that Screen Sharing allows this account.`;
}
}
prompt.print(`${retryMessage}\nDesktop settings were not changed.`);
const retry = prompt.select ? await prompt.select("Mac login", [
{ value: "retry", label: "Try password again" },
{ value: "settings", label: "Open Screen Sharing settings" },
{ value: "cancel", label: "Cancel setup" },
]) : ((await prompt.question("Try password again? [y/N]: ")).toLowerCase() === "y" ? "retry" : "cancel");
if (retry !== "retry" && retry !== "settings") return info;
if (retry === "settings") {
(options.openSettings ?? openScreenSharingSettings)();
await prompt.question("Check that your Mac account is allowed, then press Return to retry: ");
}
}
prompt.print("Desktop access is on. Open Desktop in Mindwire on your phone.\nTurn it off anytime with mindwire desktop disable.");
return client.surfaces.localStatus();
}
Loading
Loading