v3.4.1 de8f68e

route.go RawHistoryFolder

1// Package route parses Docker Registry V2 request paths into one validated
2// operation shared by authorization and dispatch. Authorization and the
3// handler must never derive the repository independently: a mismatch lets a
4// request be authorized for one repository and served from another.
5package route
6
7import (
8	"errors"
9	"fmt"
10	"strings"
11
12	"github.com/bnema/gordon/pkg/validation"
13)
14
15// Parsing errors. ErrNotFound means the path is not a registry route;
16// ErrInvalid means the route shape is recognized but a component is
17// malformed (bad repository, reference, digest, or upload id).
18var (
19	ErrNotFound = errors.New("registry route not found")
20	ErrInvalid  = errors.New("registry route invalid")
21)
22
23// Registry error codes for malformed components, matching the Docker
24// Registry V2 error format the handler returns.
25const (
26	CodeNameInvalid       = "NAME_INVALID"
27	CodeTagInvalid        = "TAG_INVALID"
28	CodeDigestInvalid     = "DIGEST_INVALID"
29	CodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID"
30)
31
32// ParseError is a malformed-component error carrying the registry error
33// code the handler must return. It wraps ErrInvalid for errors.Is.
34type ParseError struct {
35	Code string
36	Err  error
37}
38
39// Error implements error.
40func (e *ParseError) Error() string { return e.Err.Error() }
41
42// Unwrap exposes the wrapped sentinel.
43func (e *ParseError) Unwrap() error { return ErrInvalid }
44
45// invalid builds a ParseError for a malformed component.
46func invalid(code, format string, args ...any) error {
47	return &ParseError{Code: code, Err: fmt.Errorf(format, args...)}
48}
49
50// Kind classifies a registry operation.
51type Kind string
52
53// Registry operation kinds.
54const (
55	KindBase     Kind = "base"
56	KindCatalog  Kind = "catalog"
57	KindManifest Kind = "manifest"
58	KindBlob     Kind = "blob"
59	KindUpload   Kind = "upload"
60	KindTagList  Kind = "tags"
61)
62
63const (
64	manifestsSegment = "/manifests/"
65	blobsSegment     = "/blobs/"
66	uploadsSegment   = "/blobs/uploads/"
67	tagsSegment      = "/tags/list"
68)
69
70// Operation is one parsed registry request. Repository is the exact
71// repository the request addresses (empty for protocol roots), so callers
72// authorize and dispatch on the same value.
73type Operation struct {
74	Kind       Kind
75	Repository string
76	// Reference is the manifest tag or digest (manifest kind).
77	Reference string
78	// Digest is the blob digest (blob kind).
79	Digest string
80	// UploadID is the upload session id; empty marks a new upload (upload kind).
81	UploadID string
82}
83
84// RequiresRepositoryAuth reports whether the operation must be authorized
85// against a repository scope. Only protocol roots are exempt; every route
86// that names a repository is scoped.
87func (o Operation) RequiresRepositoryAuth() bool {
88	return o.Kind != KindBase && o.Kind != KindCatalog
89}
90
91// Parse classifies a registry request path. It resolves the repository
92// from the LAST route marker, matching the served route exactly, and
93// validates every component with the shared validation grammar.
94func Parse(path string) (Operation, error) {
95	if path == "/v2" || path == "/v2/" {
96		return Operation{Kind: KindBase}, nil
97	}
98	if !strings.HasPrefix(path, "/v2/") {
99		return Operation{}, ErrNotFound
100	}
101	rest := strings.TrimPrefix(path, "/v2/")
102	switch rest {
103	case "":
104		return Operation{Kind: KindBase}, nil
105	case "_catalog":
106		return Operation{Kind: KindCatalog}, nil
107	}
108
109	parsers := map[Kind]func(string) (Operation, bool, error){
110		KindTagList:  parseTags,
111		KindUpload:   parseUpload,
112		KindManifest: parseManifest,
113		KindBlob:     parseBlob,
114	}
115	markers := []struct {
116		segment string
117		kind    Kind
118	}{
119		{tagsSegment, KindTagList},
120		{uploadsSegment, KindUpload},
121		{manifestsSegment, KindManifest},
122		{blobsSegment, KindBlob},
123	}
124	// The served route is the marker that appears LAST in the path, so a
125	// reserved-looking segment earlier in the path can never shorten or
126	// redirect the repository.
127	best := -1
128	bestKind := Kind("")
129	for _, marker := range markers {
130		if idx := strings.LastIndex(rest, marker.segment); idx > best {
131			best = idx
132			bestKind = marker.kind
133		}
134	}
135	if parse, ok := parsers[bestKind]; ok {
136		if op, matched, err := parse(rest); matched {
137			return op, err
138		}
139	}
140	return Operation{}, ErrNotFound
141}
142
143// parseTags matches /{repo}/tags/list.
144func parseTags(rest string) (Operation, bool, error) {
145	repo, ok := strings.CutSuffix(rest, tagsSegment)
146	if !ok {
147		return Operation{}, false, nil
148	}
149	op, err := repositoryOperation(KindTagList, repo)
150	return op, true, err
151}
152
153// parseUpload matches /{repo}/blobs/uploads/{uuid?}.
154func parseUpload(rest string) (Operation, bool, error) {
155	idx := strings.LastIndex(rest, uploadsSegment)
156	if idx < 0 {
157		return Operation{}, false, nil
158	}
159	op, err := repositoryOperation(KindUpload, rest[:idx])
160	if err != nil {
161		return Operation{}, true, err
162	}
163	uploadID := rest[idx+len(uploadsSegment):]
164	if uploadID == "" {
165		return op, true, nil
166	}
167	if err := validation.ValidateUUID(uploadID); err != nil {
168		return Operation{}, true, invalid(CodeBlobUploadInvalid, "%s", err)
169	}
170	op.UploadID = uploadID
171	return op, true, nil
172}
173
174// parseManifest matches /{repo}/manifests/{reference}.
175func parseManifest(rest string) (Operation, bool, error) {
176	idx := strings.LastIndex(rest, manifestsSegment)
177	if idx < 0 {
178		return Operation{}, false, nil
179	}
180	reference := rest[idx+len(manifestsSegment):]
181	if reference == "" || strings.Contains(reference, "/") {
182		return Operation{}, true, ErrNotFound
183	}
184	if err := validation.ValidateReference(reference); err != nil {
185		return Operation{}, true, invalid(CodeTagInvalid, "%s", err)
186	}
187	op, err := repositoryOperation(KindManifest, rest[:idx])
188	if err != nil {
189		return Operation{}, true, err
190	}
191	op.Reference = reference
192	return op, true, nil
193}
194
195// parseBlob matches /{repo}/blobs/{digest}.
196func parseBlob(rest string) (Operation, bool, error) {
197	idx := strings.LastIndex(rest, blobsSegment)
198	if idx < 0 {
199		return Operation{}, false, nil
200	}
201	digest := rest[idx+len(blobsSegment):]
202	if digest == "" || strings.Contains(digest, "/") {
203		return Operation{}, true, ErrNotFound
204	}
205	if err := validation.ValidateDigest(digest); err != nil {
206		return Operation{}, true, invalid(CodeDigestInvalid, "%s", err)
207	}
208	op, err := repositoryOperation(KindBlob, rest[:idx])
209	if err != nil {
210		return Operation{}, true, err
211	}
212	op.Digest = digest
213	return op, true, nil
214}
215
216// repositoryOperation builds a repository-scoped operation, rejecting an
217// empty or malformed repository so a reserved-looking segment can never be
218// mistaken for a protocol root.
219func repositoryOperation(kind Kind, repo string) (Operation, error) {
220	if repo == "" || strings.HasSuffix(repo, "/") {
221		return Operation{}, ErrNotFound
222	}
223	if err := validation.ValidateRepositoryName(repo); err != nil {
224		return Operation{}, invalid(CodeNameInvalid, "%s", err)
225	}
226	return Operation{Kind: kind, Repository: repo}, nil
227}

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