fix/exec-umask 2cd66dc

guestforge.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package sandbox
4
5import (
6	"context"
7	"errors"
8	"fmt"
9	"net/netip"
10	"net/url"
11	"strconv"
12	"strings"
13	"time"
14)
15
16// EnvGuestForge is ERMITE_RUNNER_GUEST_FORGE=1: sandboxes reach the forge the
17// way the runner does, that is at the host and port of ERMITE_RUNNER_URL, on
18// the host. For staging hosts where the forge is not reachable from the
19// internet side of the NAT (see the README). Off by default.
20const EnvGuestForge = "ERMITE_RUNNER_GUEST_FORGE"
21
22// GuestForge is the one forge endpoint that sandboxes may reach on the host.
23// It is derived from the server URL by ParseGuestForge.
24type GuestForge struct {
25	// Host is the lower-case DNS name of the forge, as in the URL. The DNS
26	// forwarder answers it with the gateway address of each sandbox.
27	Host string
28	// Port is the TCP port: the only host port a sandbox may reach.
29	Port uint16
30	// Origin is scheme://host[:port] of the URL: the jobs' server URLs are
31	// rewritten to it.
32	Origin string
33}
34
35// Warning is the line logged at start and for each job.
36func (g *GuestForge) Warning() string {
37	return "guest forge access: sandboxes may reach " + g.Origin + " on the host"
38}
39
40// DNSName is the overridden name, "" when the access is off. Safe on nil.
41func (g *GuestForge) DNSName() string {
42	if g == nil {
43		return ""
44	}
45	return g.Host
46}
47
48// TCPPort is the allowed port, 0 when the access is off. Safe on nil.
49func (g *GuestForge) TCPPort() uint16 {
50	if g == nil {
51		return 0
52	}
53	return g.Port
54}
55
56// ParseGuestForge derives the endpoint from the server URL. The URL must have
57// a DNS host name (an IP literal cannot be redirected by the DNS forwarder),
58// no path, query or credentials, and the port defaults from the scheme (443,
59// 80). A plain http URL is accepted only with insecureHTTP (ERMITE_RUNNER_
60// INSECURE_HTTP=1): the job tokens then cross the host in clear, and that must
61// be asked for explicitly even for localhost.
62func ParseGuestForge(raw string, insecureHTTP bool) (*GuestForge, error) {
63	u, err := url.Parse(raw)
64	if err != nil || u.Host == "" {
65		return nil, fmt.Errorf("%s: the server URL %q is not a valid URL", EnvGuestForge, raw)
66	}
67	if u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") {
68		return nil, fmt.Errorf("%s: the server URL must be a bare origin (scheme://host[:port]): the jobs' URLs are rewritten to the origin alone", EnvGuestForge)
69	}
70	var def uint16
71	switch u.Scheme {
72	case "https":
73		def = 443
74	case "http":
75		def = 80
76		if !insecureHTTP {
77			return nil, fmt.Errorf("%s: the server URL is plain http: set ERMITE_RUNNER_INSECURE_HTTP=1 to accept that sandboxes then send their tokens in clear", EnvGuestForge)
78		}
79	default:
80		return nil, fmt.Errorf("%s: the server URL must be http or https", EnvGuestForge)
81	}
82	host := strings.ToLower(strings.TrimSuffix(u.Hostname(), "."))
83	if err := CheckForgeHostname(host); err != nil {
84		return nil, fmt.Errorf("%s: %w", EnvGuestForge, err)
85	}
86	port := def
87	if p := u.Port(); p != "" {
88		n, err := strconv.Atoi(p)
89		if err != nil || n < 1 || n > 65535 {
90			return nil, fmt.Errorf("%s: port %q is not in 1-65535", EnvGuestForge, p)
91		}
92		port = uint16(n)
93	}
94	origin := u.Scheme + "://" + host
95	if u.Port() != "" {
96		origin += ":" + u.Port()
97	}
98	return &GuestForge{Host: host, Port: port, Origin: origin}, nil
99}
100
101// CheckForgeHostname accepts one DNS host name: dot separated labels of ASCII
102// letters, digits and hyphens (not at either end), at most 63 bytes each and
103// 253 in all. IP literals, wildcards, underscores, IDNs and empty labels are
104// refused.
105func CheckForgeHostname(h string) error {
106	if h == "" || len(h) > 253 {
107		return errors.New("the host name is empty or longer than 253 bytes")
108	}
109	if _, err := netip.ParseAddr(h); err == nil {
110		return fmt.Errorf("%q is an IP address: a host name is required (sandboxes are pointed at the forge by DNS)", h)
111	}
112	labels := strings.Split(h, ".")
113	allNumeric := true
114	for _, l := range labels {
115		if l == "" || len(l) > 63 || l[0] == '-' || l[len(l)-1] == '-' {
116			return fmt.Errorf("%q is not a valid host name", h)
117		}
118		numeric := true
119		for _, r := range l {
120			ok := r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-'
121			if !ok {
122				return fmt.Errorf("%q is not a valid host name (wildcards, underscores and non-ASCII are refused)", h)
123			}
124			if r < '0' || r > '9' {
125				numeric = false
126			}
127		}
128		allNumeric = allNumeric && numeric
129	}
130	if allNumeric {
131		// "10.1" or "2130706433": legacy spellings of an address.
132		return fmt.Errorf("%q looks like an IP address: a host name is required", h)
133	}
134	return nil
135}
136
137// ResolveGuestForge is ParseGuestForge plus a check that the host name
138// resolves on the host (lookup is net.DefaultResolver.LookupHost in
139// production): the runner reaches the forge by that name, so a name that
140// resolves to nothing is a configuration error to report at start.
141func ResolveGuestForge(ctx context.Context, raw string, insecureHTTP bool, lookup func(ctx context.Context, host string) ([]string, error)) (*GuestForge, error) {
142	g, err := ParseGuestForge(raw, insecureHTTP)
143	if err != nil {
144		return nil, err
145	}
146	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
147	defer cancel()
148	addrs, err := lookup(ctx, g.Host)
149	if err != nil {
150		return nil, fmt.Errorf("%s: %s does not resolve on this host: %w", EnvGuestForge, g.Host, err)
151	}
152	if len(addrs) == 0 {
153		return nil, fmt.Errorf("%s: %s resolves to no address on this host", EnvGuestForge, g.Host)
154	}
155	return g, nil
156}

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 4

Tags 11

↑↓ moveenter openesc close