v3.3.0 b164fe9

local.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close