mirror of
https://github.com/imjasonh/kontain.me
synced 2026-07-08 09:04:54 +00:00
187 lines
4.7 KiB
Go
187 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/go-containerregistry/pkg/authn"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
|
"github.com/imjasonh/kontain.me/pkg/serve"
|
|
)
|
|
|
|
func main() {
|
|
http.Handle("/v2/", &server{
|
|
info: log.New(os.Stdout, "I ", log.Ldate|log.Ltime|log.Lshortfile),
|
|
error: log.New(os.Stderr, "E ", log.Ldate|log.Ltime|log.Lshortfile),
|
|
})
|
|
http.Handle("/", http.RedirectHandler("https://github.com/imjasonh/kontain.me/blob/main/cmd/flatten", http.StatusSeeOther))
|
|
|
|
log.Println("Starting...")
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
log.Printf("Defaulting to port %s", port)
|
|
}
|
|
log.Printf("Listening on port %s", port)
|
|
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
|
|
}
|
|
|
|
type server struct {
|
|
info, error *log.Logger
|
|
}
|
|
|
|
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
s.info.Println("handler:", r.Method, r.URL)
|
|
path := strings.TrimPrefix(r.URL.String(), "/v2/")
|
|
parts := strings.Split(path, "/")
|
|
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
serve.Error(w, serve.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case r.URL.Path == "/v2/":
|
|
// API Version check.
|
|
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
|
|
return
|
|
case parts[len(parts)-2] == "blobs":
|
|
s.handleBlobs(w, r)
|
|
case parts[len(parts)-2] == "manifests":
|
|
s.handleManifests(w, r)
|
|
default:
|
|
serve.Error(w, serve.ErrNotFound)
|
|
}
|
|
}
|
|
|
|
func reqToRef(r *http.Request) (name.Reference, error) {
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
parts = parts[1:]
|
|
target := parts[len(parts)-1]
|
|
repo := strings.Join(parts[1:len(parts)-2], "/")
|
|
|
|
if strings.Contains(target, ":") {
|
|
return name.ParseReference(repo + "@" + target)
|
|
}
|
|
return name.ParseReference(repo + ":" + target)
|
|
}
|
|
|
|
func (s *server) handleBlobs(w http.ResponseWriter, r *http.Request) {
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
parts = parts[1:]
|
|
if parts[len(parts)-1] == "" {
|
|
parts = parts[:len(parts)-1]
|
|
}
|
|
// Must have a path of form /v2/{name}/blobs/sha256:...
|
|
if len(parts) < 4 {
|
|
serve.Error(w, serve.ErrNotFound)
|
|
return
|
|
}
|
|
|
|
ref, err := reqToRef(r)
|
|
if err != nil {
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
|
|
tr, err := transport.NewWithContext(r.Context(), ref.Context().Registry, authn.Anonymous, http.DefaultTransport, []string{ref.Scope(transport.PullScope)})
|
|
if err != nil {
|
|
s.error.Println(err)
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
|
|
dig := parts[len(parts)-1]
|
|
url := "https://" + ref.Context().RegistryStr() + "/v2/" + ref.Context().String() + "/blobs/" + dig
|
|
s.info.Println("url:", url)
|
|
req, err := http.NewRequest("HEAD", url, nil)
|
|
if err != nil {
|
|
s.error.Println(err)
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
res, err := tr.RoundTrip(req)
|
|
if err != nil {
|
|
s.error.Println(err)
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
w.Header().Set("Location", res.Header.Get("Location"))
|
|
w.WriteHeader(res.StatusCode)
|
|
}
|
|
|
|
func (s *server) handleManifests(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
ref, err := reqToRef(r)
|
|
if err != nil {
|
|
s.error.Println(err)
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
|
|
desc, err := remote.Get(ref)
|
|
if err != nil {
|
|
s.error.Println(err)
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
|
|
if _, ok := ref.(name.Digest); !ok {
|
|
// Request isn't by digest; let's check it!
|
|
// Look up ref in rekor log.
|
|
cur := desc.Digest
|
|
got, err := s.lookup(ctx, ref)
|
|
if err == errRekordNotFound {
|
|
// Ref wasn't found, record it.
|
|
if err := s.record(ctx, ref, cur); err != nil {
|
|
s.error.Printf("ERROR (record): %v", ref, err)
|
|
serve.Error(w, err)
|
|
return
|
|
}
|
|
} else if err != nil {
|
|
// Lookup failed!
|
|
s.error.Printf("ERROR (lookup): %v", ref, err)
|
|
serve.Error(w, err)
|
|
return
|
|
} else {
|
|
s.info.Printf("rekor: %s -> %s", ref, got)
|
|
if got != cur {
|
|
s.error.Printf("ERROR (mismatch): got %q, want %q", got, cur)
|
|
serve.Error(w, fmt.Errorf("rekor digest mismatch: got %q, want %q", got, cur))
|
|
return
|
|
}
|
|
// Otherwise, proceed and redirect.
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
|
|
w.Header().Set("Content-Type", string(desc.MediaType))
|
|
w.Header().Set("Content-Length", strconv.Itoa(int(desc.Size)))
|
|
w.WriteHeader(http.StatusOK)
|
|
if r.Method == http.MethodGet {
|
|
io.Copy(w, bytes.NewReader(desc.Manifest))
|
|
}
|
|
}
|
|
|
|
var errRekordNotFound = errors.New("record not found in rekor")
|
|
|
|
func (s *server) lookup(ctx context.Context, ref name.Reference) (v1.Hash, error) {
|
|
// TODO implement
|
|
return v1.Hash{}, errRekordNotFound
|
|
}
|
|
|
|
func (s *server) record(ctx context.Context, ref name.Reference, digest v1.Hash) error {
|
|
// TODO implement
|
|
return nil
|
|
}
|