From b8b7ac87f9e5dc078d983e9a037c3a4ddc2e48d2 Mon Sep 17 00:00:00 2001 From: esengine <359807859@qq.com> Date: Mon, 26 Oct 2026 12:24:35 -0700 Subject: [PATCH] fix(serve): a paired device opens only folders the window already lists --- internal/frontend/serve/hub.go | 4 ++ internal/frontend/serve/hub_device.go | 29 +++++++++ .../serve/runtime_device_root_test.go | 59 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 internal/frontend/serve/hub_device.go create mode 100644 internal/frontend/serve/runtime_device_root_test.go diff --git a/internal/frontend/serve/hub.go b/internal/frontend/serve/hub.go index b0c6f65ad4..21541279da 100644 --- a/internal/frontend/serve/hub.go +++ b/internal/frontend/serve/hub.go @@ -583,6 +583,10 @@ func (h *Hub) openRuntime(w http.ResponseWriter, r *http.Request) { badBody(w) return } + if !hostReach(r) && !deviceMayOpenRoot(req.Root, h.knownRoots()) { + refuse(w, http.StatusNotFound, codeDeviceHostOnly, "a paired device cannot use this", nil) + return + } rt, err := h.Open(r.Context(), req) if err != nil { // Every refusal Open produces carries its own status; what is left is diff --git a/internal/frontend/serve/hub_device.go b/internal/frontend/serve/hub_device.go new file mode 100644 index 0000000000..671e9e5dea --- /dev/null +++ b/internal/frontend/serve/hub_device.go @@ -0,0 +1,29 @@ +package serve + +import "path/filepath" + +// deviceMayOpenRoot reports whether a paired device may open a pane on root: the +// default folder, or one the window already lists. A folder the device names +// itself would point the agent at whatever directory this machine can read. +func deviceMayOpenRoot(root string, known []string) bool { + if root == "" { + return true + } + want := filepath.Clean(root) + for _, k := range known { + if filepath.Clean(k) == want { + return true + } + } + return false +} + +// knownRoots are the folders open as panes now and the project list the +// sidebar remembers. +func (h *Hub) knownRoots() []string { + roots := Workspaces() + for _, v := range h.List() { + roots = append(roots, v.Root) + } + return roots +} diff --git a/internal/frontend/serve/runtime_device_root_test.go b/internal/frontend/serve/runtime_device_root_test.go new file mode 100644 index 0000000000..8c38d3c981 --- /dev/null +++ b/internal/frontend/serve/runtime_device_root_test.go @@ -0,0 +1,59 @@ +package serve + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "reasonix/internal/base/testenv" + "reasonix/internal/contract/config" + "reasonix/internal/session/control" +) + +func TestADeviceOpensOnlyFoldersTheWindowAlreadyListsAsRuntimes(t *testing.T) { + t.Setenv("REASONIX_HOME", testenv.TempDir(t)) + hub := NewHub(HubOptions{Serve: config.ServeConfig{AuthMode: "none"}}) + defer hub.Shutdown() + bc := NewBroadcaster() + if _, err := hub.Adopt(New(control.New(control.Options{Sink: bc}), bc, config.ServeConfig{}), bc); err != nil { + t.Fatal(err) + } + before := len(hub.List()) + elsewhere := testenv.TempDir(t) + + reg := NewDeviceRegistry() + code, _ := reg.Offer() + cred, _, err := reg.Redeem(code, "phone") + if err != nil { + t.Fatal(err) + } + gate := NewDeviceGate(hub.Handler(), DeviceGateOptions{Registry: reg, Origin: "http://192.168.1.5:9000"}) + req := httptest.NewRequest(http.MethodPost, "http://192.168.1.5:9000/runtimes", strings.NewReader(`{"root":`+quoteJSON(elsewhere)+`}`)) + req.Header.Set("Origin", "http://192.168.1.5:9000") + req.Header.Set("Content-Type", "application/json") + req.AddCookie(&http.Cookie{Name: DeviceCookie, Value: cred}) + rec := httptest.NewRecorder() + gate.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), codeDeviceHostOnly) { + t.Fatalf("a device opening an unlisted folder: %d %s, want 404 %s", rec.Code, rec.Body.String(), codeDeviceHostOnly) + } + if after := len(hub.List()); after != before { + t.Fatalf("a runtime was opened for the device: %d -> %d", before, after) + } +} + +func TestDeviceMayOpenRootNamesOnlyKnownFolders(t *testing.T) { + known := []string{"/work/a", "/work/b"} + for root, want := range map[string]bool{"": true, "/work/a": true, "/work/a/": true, "/work/b/../b": true, "/work": false, "/etc": false, "/work/a/sub": false} { + if got := deviceMayOpenRoot(root, known); got != want { + t.Errorf("deviceMayOpenRoot(%q) = %v, want %v", root, got, want) + } + } +} + +func quoteJSON(s string) string { + b, _ := json.Marshal(s) + return string(b) +}