197 lines
5.5 KiB
Go
197 lines
5.5 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package control
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
func (s *Server) endpoint_snapshot() []endpoint {
|
|
s.endpointsMu.RLock()
|
|
defer s.endpointsMu.RUnlock()
|
|
return slices.Clone(s.endpoints)
|
|
}
|
|
|
|
// Every default instance has a private endpoint. The first available instance
|
|
// also serves the shared path, and a survivor claims it after that owner exits.
|
|
func (s *Server) claim_main() bool {
|
|
if s.options.SocketFile != "" || s.ctx.Err() != nil {
|
|
return false
|
|
}
|
|
s.endpointsMu.Lock()
|
|
defer s.endpointsMu.Unlock()
|
|
for _, endpoint := range s.endpoints {
|
|
if endpoint.path == s.main_path() {
|
|
return false
|
|
}
|
|
}
|
|
endpoint, err := listen(s.main_path())
|
|
if err != nil {
|
|
return false // Active readers and unrelated paths are preserved.
|
|
}
|
|
if s.ctx.Err() != nil {
|
|
endpoint.listener.Close()
|
|
if info, err := os.Lstat(endpoint.path); err == nil && os.SameFile(info, endpoint.info) {
|
|
_ = os.Remove(endpoint.path)
|
|
}
|
|
return false
|
|
}
|
|
s.endpoints = append(s.endpoints, endpoint)
|
|
s.workers.Go(func() { s.accept(endpoint) })
|
|
return true
|
|
}
|
|
|
|
func (s *Server) private_path(data instance) string {
|
|
path := filepath.Join(filepath.Dir(s.main_path()), fmt.Sprintf("fenrirscreenreader-%d.sock", data.PID))
|
|
if slices.Contains(data.SocketFiles, path) {
|
|
return path
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Ancestry identifies ordinary PTY children. Reattached Screen sessions and
|
|
// detached helpers can lose that ancestry, so also use the caller's own X11
|
|
// identity. Neither a single terminal nor a console is a safe default.
|
|
func (s *Server) route_target(pid, uid int) (string, error) {
|
|
instances := s.instances()
|
|
var terminals, consoles []instance
|
|
for _, data := range instances {
|
|
if s.private_path(data) == "" {
|
|
continue
|
|
}
|
|
if data.ScreenDriver == "vcsaDriver" && (data.uid == 0 || data.uid == uid) {
|
|
consoles = append(consoles, data)
|
|
} else if data.ScreenDriver == "ptyDriver" && (data.uid == uid || uid == 0) {
|
|
terminals = append(terminals, data)
|
|
}
|
|
}
|
|
seen := map[int]bool{}
|
|
var ancestors []int
|
|
var ancestorReader *instance
|
|
ancestry:
|
|
for ancestor := pid; ancestor > 1 && !seen[ancestor]; ancestor = parent_pid(ancestor) {
|
|
seen[ancestor] = true
|
|
ancestors = append(ancestors, ancestor)
|
|
for _, data := range terminals {
|
|
if data.PID == ancestor {
|
|
ancestorReader = &data
|
|
break ancestry
|
|
}
|
|
}
|
|
for _, data := range consoles {
|
|
if data.PID == ancestor {
|
|
ancestorReader = &data
|
|
break ancestry
|
|
}
|
|
}
|
|
}
|
|
readers := append(slices.Clone(terminals), consoles...)
|
|
if reader, handled, err := attached_screen_reader("/proc", ancestors, uid, readers); handled {
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return s.destination(reader)
|
|
}
|
|
if ancestorReader != nil {
|
|
return s.destination(*ancestorReader)
|
|
}
|
|
identity := process_identity(pid, uid)
|
|
return s.route_identity(identity, uid, terminals, consoles)
|
|
}
|
|
|
|
type processIdentity struct {
|
|
window, display string
|
|
console bool
|
|
}
|
|
|
|
// Read only identity fields from the credentialed process, never a supplied PID
|
|
// or environment in the request text. Environment is a routing hint, not an
|
|
// authorization grant; the destination still checks the original SO_PEERCRED.
|
|
func process_identity(pid, uid int) processIdentity {
|
|
var identity processIdentity
|
|
info, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
|
|
if err != nil {
|
|
return identity
|
|
}
|
|
if stat, ok := info.Sys().(*syscall.Stat_t); !ok || int(stat.Uid) != uid {
|
|
return identity
|
|
}
|
|
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err == nil {
|
|
end := strings.LastIndexByte(string(data), ')')
|
|
if end >= 0 {
|
|
fields := strings.Fields(string(data[end+1:]))
|
|
if len(fields) > 4 {
|
|
tty, _ := strconv.ParseInt(fields[4], 10, 64)
|
|
device := uint32(tty)
|
|
major := (device >> 8) & 0xfff
|
|
minor := (device & 0xff) | ((device >> 12) & 0xfff00)
|
|
identity.console = major == 4 && minor >= 1 && minor <= 63
|
|
}
|
|
}
|
|
}
|
|
file, err := os.Open(fmt.Sprintf("/proc/%d/environ", pid))
|
|
if err != nil {
|
|
return identity
|
|
}
|
|
defer file.Close()
|
|
data, err := io.ReadAll(io.LimitReader(file, 1<<20))
|
|
if err != nil {
|
|
return identity
|
|
}
|
|
for _, entry := range strings.Split(string(data), "\x00") {
|
|
key, value, _ := strings.Cut(entry, "=")
|
|
switch key {
|
|
case "WINDOWID":
|
|
identity.window = value
|
|
case "DISPLAY":
|
|
identity.display = value
|
|
}
|
|
}
|
|
return identity
|
|
}
|
|
|
|
func (s *Server) route_identity(identity processIdentity, uid int, terminals, consoles []instance) (string, error) {
|
|
var matches []instance
|
|
if identity.console {
|
|
// A real controlling VT takes precedence over inherited X11 variables.
|
|
matches = consoles
|
|
} else if identity.window != "" && identity.display != "" {
|
|
for _, data := range terminals {
|
|
if data.uid != uid || data.WindowID != identity.window {
|
|
continue
|
|
}
|
|
target := process_identity(data.PID, data.uid)
|
|
if target.display == identity.display {
|
|
matches = append(matches, data)
|
|
}
|
|
}
|
|
}
|
|
if len(matches) == 1 {
|
|
return s.destination(matches[0])
|
|
}
|
|
return "", fmt.Errorf("caller does not identify a unique reader; run inside its managed terminal or use its private socket")
|
|
}
|
|
|
|
func (s *Server) destination(data instance) (string, error) {
|
|
if data.PID == os.Getpid() {
|
|
return "", nil
|
|
}
|
|
if !data.ConnectionHandoff {
|
|
return "", fmt.Errorf("reader %d cannot accept a connection handoff", data.PID)
|
|
}
|
|
path := s.private_path(data)
|
|
info, err := os.Lstat(path)
|
|
if err != nil || info.Mode()&os.ModeSocket == 0 {
|
|
return "", fmt.Errorf("reader %d private socket is unavailable", data.PID)
|
|
}
|
|
return path, nil
|
|
}
|