1package remote
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "net"
9 "net/http"
10 "os"
11 "path/filepath"
12 "syscall"
13 "time"
14
15 "github.com/bnema/gordon/internal/adapters/localadmin"
16)
17
18// LocalBaseURL is the synthetic origin of the Unix-socket admin client. The
19// transport ignores the host and always dials the owner-only socket.
20const LocalBaseURL = "http://gordon.local"
21
22// ErrDaemonUnavailable reports that no owner-only admin socket passed
23// validation, so no local daemon can be reached.
24var ErrDaemonUnavailable = errors.New("daemon-unavailable")
25
26// errLocalRedirect refuses redirects on the local socket: a redirect can never
27// be a legitimate part of the owner-only admin surface.
28var errLocalRedirect = errors.New("local admin client does not follow redirects")
29
30// localSocketDirs lists the discovery candidates in daemon priority order.
31// It is a variable so tests can isolate from a real daemon.
32var localSocketDirs = localadmin.ClientDirCandidates
33
34// DiscoverLocalSocket returns the first admin socket candidate, in daemon
35// priority order, that is an owner-owned Unix socket without group/other
36// permission bits.
37func DiscoverLocalSocket() (string, error) {
38 return DiscoverLocalSocketIn(localSocketDirs())
39}
40
41// DiscoverLocalSocketIn is DiscoverLocalSocket over explicit candidate
42// directories. Each candidate is validated, never followed through symlinks.
43func DiscoverLocalSocketIn(dirs []string) (string, error) {
44 var lastErr error
45 for _, dir := range dirs {
46 path := filepath.Join(dir, localadmin.SocketName)
47 if _, err := os.Lstat(path); err != nil {
48 lastErr = err
49 continue
50 }
51 if err := localadmin.ValidateDir(dir); err != nil {
52 return "", fmt.Errorf("%w: unsafe local admin directory: %w", ErrDaemonUnavailable, err)
53 }
54 if err := localadmin.ValidateSocket(path); err != nil {
55 return "", fmt.Errorf("%w: unsafe local admin socket: %w", ErrDaemonUnavailable, err)
56 }
57 live, err := localadmin.ProbeSocket(path)
58 if err != nil {
59 return "", fmt.Errorf("%w: probe local admin socket: %w", ErrDaemonUnavailable, err)
60 }
61 if !live {
62 lastErr = syscall.ECONNREFUSED
63 continue
64 }
65 return path, nil
66 }
67 if lastErr == nil {
68 lastErr = fs.ErrNotExist
69 }
70 return "", fmt.Errorf("%w: no owner-only admin socket found: %w", ErrDaemonUnavailable, lastErr)
71}
72
73// NewLocalClient discovers the daemon's owner-only admin socket and returns a
74// client bound to it. It returns ErrDaemonUnavailable when no daemon is
75// reachable; callers must not fall back to local writes.
76func NewLocalClient() (*Client, error) {
77 socketPath, err := DiscoverLocalSocket()
78 if err != nil {
79 return nil, err
80 }
81 return NewLocalClientForSocket(socketPath), nil
82}
83
84// NewLocalClientForSocket returns a client whose transport dials only the
85// given Unix socket. It sends no Authorization header and never exchanges a
86// token, so it works with auth.enabled=false.
87func NewLocalClientForSocket(socketPath string) *Client {
88 transport := &http.Transport{
89 // Never consult environment proxies for the local socket. DialContext
90 // authenticates SO_PEERCRED before the transport can write request bytes.
91 Proxy: nil,
92 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
93 return localadmin.DialContext(ctx, socketPath)
94 },
95 }
96
97 httpClient := &http.Client{
98 Transport: transport,
99 Timeout: 2 * time.Minute,
100 CheckRedirect: func(*http.Request, []*http.Request) error {
101 return errLocalRedirect
102 },
103 }
104
105 return NewClient(LocalBaseURL, WithHTTPClient(httpClient))
106}