From 19b173357a3e834b9f9bb03aec6b82fba31023d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9E=D0=BB=D0=B5=D0=B3=20=D0=91=D0=BE=D1=80=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 26 Feb 2026 18:15:10 +0200 Subject: [PATCH] working commit --- authbas.go | 24 +++++++++++++++++++ client.go | 32 ++++++++++++++++++++++++++ digest.go | 50 ++++++++++++++++++++++++++++++++++++++++ getman.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ getman_test.go | 34 +++++++++++++++++++++++++++ go.mod | 11 +++++++++ go.sum | 10 ++++++++ putman.go | 50 ++++++++++++++++++++++++++++++++++++++++ refer.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 328 insertions(+) create mode 100644 authbas.go create mode 100644 client.go create mode 100644 digest.go create mode 100644 getman.go create mode 100644 getman_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 putman.go create mode 100644 refer.go diff --git a/authbas.go b/authbas.go new file mode 100644 index 0000000..c26dfb0 --- /dev/null +++ b/authbas.go @@ -0,0 +1,24 @@ +package client + +import ( + "encoding/base64" +) + + +type Authenticator interface { + MakeHeader(user, pass string) (key, value string, err error) +} + +type BasicAuthenticator struct {} + +func NewBasicAuthenticator() *BasicAuthenticator{ + return &BasicAuthenticator{} +} + +func (auth *BasicAuthenticator) MakeHeader(user, pass string) (string, string, error){ + pair := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) + return "Autentification", "Basic " + pair, nil +} + + + diff --git a/client.go b/client.go new file mode 100644 index 0000000..78e7779 --- /dev/null +++ b/client.go @@ -0,0 +1,32 @@ +package client + +import ( + "crypto/tls" + "net/http" +) + + +type Client struct { + httpClient *http.Client + authenticator Authenticator + userAgent string +} + +func NewClient(skipTLSVerify bool) *Client { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: skipTLSVerify, + }, + } + httpClient := &http.Client{ + Transport: transport, + } + return &Client{ + httpClient: httpClient, + userAgent: "ociClient/1.0", + } +} + +func (cli *Client) SetAuthenticator(auth Authenticator) { + cli.authenticator = auth +} diff --git a/digest.go b/digest.go new file mode 100644 index 0000000..becbe6e --- /dev/null +++ b/digest.go @@ -0,0 +1,50 @@ +package client + +import ( + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "strings" +) + +func SHA256Digest(src []byte) string { + hasher := sha256.New() + hasher.Write(src) + sum := hasher.Sum(nil) + return "sha256:" + hex.EncodeToString(sum) +} + +func SHA512Digest(src []byte) string { + hasher := sha512.New() + hasher.Write(src) + sum := hasher.Sum(nil) + return "sha512:" + hex.EncodeToString(sum) +} + +const ( + Undefined int = iota + SHA256 + SHA512 +) + + +func DigestType(digest string) int { + var err error + var typ int + digest = strings.ToLower(digest) + digest = strings.TrimPrefix(digest, "sha256:") + digest = strings.TrimPrefix(digest, "sha512:") + decoded, err := hex.DecodeString(digest) + if err != nil { + return Undefined + } + switch (len(decoded)) { + case 64: + typ = SHA256 + case 128: + typ = SHA512 + default: + typ = Undefined + } + return typ +} diff --git a/getman.go b/getman.go new file mode 100644 index 0000000..cf66473 --- /dev/null +++ b/getman.go @@ -0,0 +1,62 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strconv" +) + +func (cli *Client) GetManifest(ctx context.Context, rawref string) (bool, string, []byte, error) { + var err error + var exist bool + var mime string + var man []byte + + ref, err := NewReference(rawref) + if err != nil { + return exist, mime, man, err + } + uri := ref.Manifest() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return exist, mime, man, err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Accept", "*/*") + resp, err := cli.httpClient.Do(req) + if err != nil { + return exist, mime, man, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return exist, mime, man, err + } + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("Unxected response code %s", resp.Status) + return exist, mime, man, err + } + contentLength := resp.Header.Get("Content-Length") + manSize, err := strconv.ParseInt(contentLength, 10, 64) + if err != nil { + return exist, mime, man, err + } + mime = resp.Header.Get("Content-Type") + if mime == "" { + err := fmt.Errorf("Empty MIME type declaration") + return exist, mime, man, err + } + buffer := bytes.NewBuffer(nil) + recSize, err := io.Copy(buffer, resp.Body) + if manSize != recSize { + err := fmt.Errorf("Mismatch declared and actual body size") + return exist, mime, man, err + } + man = buffer.Bytes() + exist = true + return exist, mime, man, err +} diff --git a/getman_test.go b/getman_test.go new file mode 100644 index 0000000..4fab84c --- /dev/null +++ b/getman_test.go @@ -0,0 +1,34 @@ +package client + +import ( + "github.com/stretchr/testify/require" + + "testing" + "fmt" + "time" + "context" + "encoding/json" + "bytes" +) + + +func TestClientGetManigest(t *testing.T) { + rawrefs := []string{ + "mirror.gcr.io/alpine:3.20.0", + "mirror.gcr.io/alpine:sha256:29e5ba63e79337818e6c63cfcc68e2ab4e9ca483853b2de303bfbfba9372426c", + } + for _, rawref := range rawrefs { + cli := NewClient(true) + ctx, _ := context.WithTimeout(context.Background(), 10 * time.Second) + exist, mime, man, err := cli.GetManifest(ctx, rawref) + require.NoError(t, err) + require.True(t, exist) + + fmt.Printf("MIME: %s\n", mime) + buffer := bytes.NewBuffer(nil) + err = json.Indent(buffer, man, " ", " ") + require.NoError(t, err) + fmt.Printf("%s\n", buffer.String()) + } +} + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e473a6b --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module client + +go 1.25.0 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/putman.go b/putman.go new file mode 100644 index 0000000..6552d6a --- /dev/null +++ b/putman.go @@ -0,0 +1,50 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "net/http" +) + +func (cli *Client) PutManifest(ctx context.Context, rawref string, man []byte, mime string) error { + var err error + + ref, err := NewReference(rawref) + if err != nil { + return err + } + uri := ref.Manifest() + user, pass := ref.Userinfo() + + buffer := bytes.NewBuffer(man) + req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri, buffer) + if err != nil { + return err + } + req.Header.Set("User-Agent", cli.userAgent) + req.Header.Set("Docker-Content-Digest", SHA256Digest(man)) + req.Header.Set("Content-Type", mime) + if cli.authenticator != nil { + authHeader, authKey, err := cli.authenticator.MakeHeader(user, pass) + if err != nil { + return err + } + req.Header.Set(authHeader, authKey) + } + resp, err := cli.httpClient.Do(req) + if err != nil { + return err + } + resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + err = fmt.Errorf("Manifest not accepted, code %d", resp.StatusCode) + return err + } + loc := resp.Header.Get("Location") + if loc == "" { + err := fmt.Errorf("Empty manifest location declaration") + return err + } + return err +} diff --git a/refer.go b/refer.go new file mode 100644 index 0000000..ae9dfc1 --- /dev/null +++ b/refer.go @@ -0,0 +1,55 @@ +package client + +import ( + "errors" + "net/url" + "strings" +) + +type Reference struct { + urlobj *url.URL + user, pass string + repo, tag string +} + +func NewReference(rawref string) (*Reference, error) { + ref := &Reference{} + if !strings.Contains(rawref, "://") { + rawref = "https://" + rawref + } + urlobj, err := url.Parse(rawref) + if err != nil { + return ref, err + } + if urlobj.User != nil { + ref.user = urlobj.User.Username() + ref.pass, _ = urlobj.User.Password() + urlobj.User = nil + } + repotag := strings.SplitN(urlobj.Path, ":", 2) + if len(repotag) != 2 { + err = errors.New("Incorrect repo") + return ref, err + } + ref.urlobj = urlobj + ref.urlobj.Path = "" + ref.repo = repotag[0] + ref.tag = repotag[1] + ref.urlobj = urlobj + + return ref, err +} + +func (ref *Reference) Manifest() string { + curl := ref.urlobj.JoinPath("/v2", ref.repo, "/manifests", ref.tag) + return curl.String() +} + +func (ref *Reference) Blob(digest string) string { + curl := ref.urlobj.JoinPath("/v2", ref.repo, "/blobs", digest) + return curl.String() +} + +func (ref *Reference) Userinfo() (string, string) { + return ref.user, ref.pass +}