fix(serve): a paired device opens only folders the window already lists

This commit is contained in:
esengine committed 2026-10-26 12:24:35 -07:00
1 parent 3463385901
commit b8b7ac87f9
3 files changed
+92

No files matched your search

+4
View File
@@ -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
+29
View File
@@ -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
}
@@ -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)
}