blob: d7765b109c20f2b26344ba7e00530878b50d118a [file] [log] [blame]
Jan Schärcc9e4d12025-04-14 10:28:40 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package registry
5
6import (
7 "bytes"
8 "encoding/json"
9 "fmt"
10 "internal/sync"
11 "io"
12 "net/http"
13 "regexp"
14 "strconv"
15 "strings"
16 "time"
17
Jan Schärcc9e4d12025-04-14 10:28:40 +000018 "source.monogon.dev/osbase/oci"
19 "source.monogon.dev/osbase/structfs"
20)
21
22var (
23 manifestsEp = regexp.MustCompile("^/v2/(" + repositoryExpr + ")/manifests/(" + tagExpr + "|" + digestExpr + ")$")
24 blobsEp = regexp.MustCompile("^/v2/(" + repositoryExpr + ")/blobs/(" + digestExpr + ")$")
25)
26
27// Server is an OCI registry server.
28type Server struct {
29 mu sync.Mutex
30 repositories map[string]*serverRepository
31}
32
33type serverRepository struct {
34 tags map[string]string
35 manifests map[string]serverManifest
36 blobs map[string]structfs.Blob
37}
38
39type serverManifest struct {
40 contentType string
41 content []byte
42}
43
44func NewServer() *Server {
45 return &Server{
46 repositories: make(map[string]*serverRepository),
47 }
48}
49
Jan Schär2963b682025-07-17 17:03:44 +020050// AddRef adds a Ref to the server in the specified repository.
Jan Schärcc9e4d12025-04-14 10:28:40 +000051//
52// If the tag is empty, the image can only be fetched by digest.
Jan Schär2963b682025-07-17 17:03:44 +020053func (s *Server) AddRef(repository string, tag string, ref oci.Ref) error {
Jan Schär62cecde2025-04-16 15:24:04 +000054 if !RepositoryRegexp.MatchString(repository) {
Jan Schärcc9e4d12025-04-14 10:28:40 +000055 return fmt.Errorf("invalid repository %q", repository)
56 }
Jan Schär62cecde2025-04-16 15:24:04 +000057 if tag != "" && !TagRegexp.MatchString(tag) {
Jan Schärcc9e4d12025-04-14 10:28:40 +000058 return fmt.Errorf("invalid tag %q", tag)
59 }
Jan Schär2963b682025-07-17 17:03:44 +020060 var refs []oci.Ref
61 err := oci.WalkRefs(ref.Digest(), ref, func(_ string, r oci.Ref) error {
62 refs = append(refs, r)
63 return nil
64 })
65 if err != nil {
66 return err
67 }
Jan Schärcc9e4d12025-04-14 10:28:40 +000068
69 s.mu.Lock()
70 defer s.mu.Unlock()
71 repo := s.repositories[repository]
72 if repo == nil {
73 repo = &serverRepository{
74 tags: make(map[string]string),
75 manifests: make(map[string]serverManifest),
76 blobs: make(map[string]structfs.Blob),
77 }
78 s.repositories[repository] = repo
79 }
Jan Schär2963b682025-07-17 17:03:44 +020080 for _, ref := range refs {
81 if _, ok := repo.manifests[ref.Digest()]; ok {
82 continue
Jan Schärcc9e4d12025-04-14 10:28:40 +000083 }
Jan Schär2963b682025-07-17 17:03:44 +020084 if image, ok := ref.(*oci.Image); ok {
85 for descriptor := range image.Descriptors() {
86 repo.blobs[string(descriptor.Digest)] = image.StructfsBlob(descriptor)
87 }
88 }
89 repo.manifests[ref.Digest()] = serverManifest{
90 contentType: ref.MediaType(),
91 content: ref.RawManifest(),
Jan Schärcc9e4d12025-04-14 10:28:40 +000092 }
93 }
94 if tag != "" {
Jan Schär2963b682025-07-17 17:03:44 +020095 repo.tags[tag] = ref.Digest()
Jan Schärcc9e4d12025-04-14 10:28:40 +000096 }
97 return nil
98}
99
100func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
101 if req.Method != "GET" && req.Method != "HEAD" {
102 http.Error(w, "Registry is read-only, only GET and HEAD are allowed", http.StatusMethodNotAllowed)
103 return
104 }
105
106 if req.URL.Path == "/v2/" {
107 w.WriteHeader(http.StatusOK)
108 } else if matches := manifestsEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
109 repository := matches[1]
110 reference := matches[2]
111 s.mu.Lock()
112 repo := s.repositories[repository]
113 if repo == nil {
114 s.mu.Unlock()
115 serveError(w, "NAME_UNKNOWN", fmt.Sprintf("Unknown repository: %s", repository), http.StatusNotFound)
116 return
117 }
118 digest := reference
119 if !strings.ContainsRune(reference, ':') {
120 var ok bool
121 digest, ok = repo.tags[reference]
122 if !ok {
123 s.mu.Unlock()
124 serveError(w, "MANIFEST_UNKNOWN", fmt.Sprintf("Unknown tag: %s", reference), http.StatusNotFound)
125 return
126 }
127 }
128 manifest, ok := repo.manifests[digest]
129 s.mu.Unlock()
130 if !ok {
131 serveError(w, "MANIFEST_UNKNOWN", fmt.Sprintf("Unknown manifest: %s", digest), http.StatusNotFound)
132 return
133 }
134
135 w.Header().Set("Docker-Content-Digest", digest)
136 w.Header().Set("Etag", fmt.Sprintf(`"%s"`, digest))
137 w.Header().Set("Content-Type", manifest.contentType)
138 w.Header().Set("X-Content-Type-Options", "nosniff")
139 http.ServeContent(w, req, "", time.Time{}, bytes.NewReader(manifest.content))
140 } else if matches := blobsEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
141 repository := matches[1]
142 digest := matches[2]
143 s.mu.Lock()
144 repo := s.repositories[repository]
145 if repo == nil {
146 s.mu.Unlock()
147 serveError(w, "NAME_UNKNOWN", fmt.Sprintf("Unknown repository: %s", repository), http.StatusNotFound)
148 return
149 }
150 blob, ok := repo.blobs[digest]
151 s.mu.Unlock()
152 if !ok {
153 serveError(w, "BLOB_UNKNOWN", fmt.Sprintf("Unknown blob: %s", digest), http.StatusNotFound)
154 return
155 }
156
157 content, err := blob.Open()
158 if err != nil {
159 http.Error(w, "Failed to open blob", http.StatusInternalServerError)
160 return
161 }
162 defer content.Close()
163 w.Header().Set("Docker-Content-Digest", digest)
164 w.Header().Set("Etag", fmt.Sprintf(`"%s"`, digest))
165 w.Header().Set("Content-Type", "application/octet-stream")
166 if contentSeeker, ok := content.(io.ReadSeeker); ok {
167 http.ServeContent(w, req, "", time.Time{}, contentSeeker)
168 } else {
169 // Range requests are not supported.
170 w.Header().Set("Content-Length", strconv.FormatInt(blob.Size(), 10))
171 w.WriteHeader(http.StatusOK)
172 if req.Method != "HEAD" {
173 io.CopyN(w, content, blob.Size())
174 }
175 }
176 } else {
177 w.WriteHeader(http.StatusNotFound)
178 }
179}
180
181func serveError(w http.ResponseWriter, code string, message string, statusCode int) {
182 w.Header().Set("Content-Type", "application/json; charset=utf-8")
183 w.Header().Set("X-Content-Type-Options", "nosniff")
184 w.WriteHeader(statusCode)
185 content, err := json.Marshal(&ErrorBody{Errors: []ErrorInfo{{
186 Code: code,
187 Message: message,
188 }}})
189 if err == nil {
190 w.Write(content)
191 }
192}