123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- package http
- import (
- "context"
- "crypto/sha256"
- "encoding/json"
- "fmt"
- "io"
- "mime"
- "net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "strings"
- "time"
- "github.com/docker/docker/pkg/locker"
- "github.com/moby/buildkit/cache"
- "github.com/moby/buildkit/cache/metadata"
- "github.com/moby/buildkit/snapshot"
- "github.com/moby/buildkit/source"
- "github.com/moby/buildkit/util/tracing"
- digest "github.com/opencontainers/go-digest"
- "github.com/pkg/errors"
- bolt "go.etcd.io/bbolt"
- )
- type Opt struct {
- CacheAccessor cache.Accessor
- MetadataStore *metadata.Store
- Transport http.RoundTripper
- }
- type httpSource struct {
- md *metadata.Store
- cache cache.Accessor
- locker *locker.Locker
- client *http.Client
- }
- func NewSource(opt Opt) (source.Source, error) {
- transport := opt.Transport
- if transport == nil {
- transport = tracing.DefaultTransport
- }
- hs := &httpSource{
- md: opt.MetadataStore,
- cache: opt.CacheAccessor,
- locker: locker.New(),
- client: &http.Client{
- Transport: transport,
- },
- }
- return hs, nil
- }
- func (hs *httpSource) ID() string {
- return source.HttpsScheme
- }
- type httpSourceHandler struct {
- *httpSource
- src source.HttpIdentifier
- refID string
- cacheKey digest.Digest
- }
- func (hs *httpSource) Resolve(ctx context.Context, id source.Identifier) (source.SourceInstance, error) {
- httpIdentifier, ok := id.(*source.HttpIdentifier)
- if !ok {
- return nil, errors.Errorf("invalid http identifier %v", id)
- }
- return &httpSourceHandler{
- src: *httpIdentifier,
- httpSource: hs,
- }, nil
- }
- // urlHash is internal hash the etag is stored by that doesn't leak outside
- // this package.
- func (hs *httpSourceHandler) urlHash() (digest.Digest, error) {
- dt, err := json.Marshal(struct {
- Filename string
- Perm, UID, GID int
- }{
- Filename: getFileName(hs.src.URL, hs.src.Filename, nil),
- Perm: hs.src.Perm,
- UID: hs.src.UID,
- GID: hs.src.GID,
- })
- if err != nil {
- return "", err
- }
- return digest.FromBytes(dt), nil
- }
- func (hs *httpSourceHandler) formatCacheKey(filename string, dgst digest.Digest, lastModTime string) digest.Digest {
- dt, err := json.Marshal(struct {
- Filename string
- Perm, UID, GID int
- Checksum digest.Digest
- LastModTime string `json:",omitempty"`
- }{
- Filename: filename,
- Perm: hs.src.Perm,
- UID: hs.src.UID,
- GID: hs.src.GID,
- Checksum: dgst,
- LastModTime: lastModTime,
- })
- if err != nil {
- return dgst
- }
- return digest.FromBytes(dt)
- }
- func (hs *httpSourceHandler) CacheKey(ctx context.Context, index int) (string, bool, error) {
- if hs.src.Checksum != "" {
- hs.cacheKey = hs.src.Checksum
- return hs.formatCacheKey(getFileName(hs.src.URL, hs.src.Filename, nil), hs.src.Checksum, "").String(), true, nil
- }
- uh, err := hs.urlHash()
- if err != nil {
- return "", false, nil
- }
- // look up metadata(previously stored headers) for that URL
- sis, err := hs.md.Search(uh.String())
- if err != nil {
- return "", false, errors.Wrapf(err, "failed to search metadata for %s", uh)
- }
- req, err := http.NewRequest("GET", hs.src.URL, nil)
- if err != nil {
- return "", false, err
- }
- req = req.WithContext(ctx)
- m := map[string]*metadata.StorageItem{}
- if len(sis) > 0 {
- for _, si := range sis {
- // if metaDigest := getMetaDigest(si); metaDigest == hs.formatCacheKey("") {
- if etag := getETag(si); etag != "" {
- if dgst := getChecksum(si); dgst != "" {
- m[etag] = si
- req.Header.Add("If-None-Match", etag)
- }
- }
- // }
- }
- }
- resp, err := hs.client.Do(req)
- if err != nil {
- return "", false, err
- }
- if resp.StatusCode < 200 || resp.StatusCode >= 400 {
- return "", false, errors.Errorf("invalid response status %d", resp.StatusCode)
- }
- if resp.StatusCode == http.StatusNotModified {
- respETag := resp.Header.Get("ETag")
- si, ok := m[respETag]
- if !ok {
- return "", false, errors.Errorf("invalid not-modified ETag: %v", respETag)
- }
- hs.refID = si.ID()
- dgst := getChecksum(si)
- if dgst == "" {
- return "", false, errors.Errorf("invalid metadata change")
- }
- modTime := getModTime(si)
- resp.Body.Close()
- return hs.formatCacheKey(getFileName(hs.src.URL, hs.src.Filename, resp), dgst, modTime).String(), true, nil
- }
- ref, dgst, err := hs.save(ctx, resp)
- if err != nil {
- return "", false, err
- }
- ref.Release(context.TODO())
- hs.cacheKey = dgst
- return hs.formatCacheKey(getFileName(hs.src.URL, hs.src.Filename, resp), dgst, resp.Header.Get("Last-Modified")).String(), true, nil
- }
- func (hs *httpSourceHandler) save(ctx context.Context, resp *http.Response) (ref cache.ImmutableRef, dgst digest.Digest, retErr error) {
- newRef, err := hs.cache.New(ctx, nil, cache.CachePolicyRetain, cache.WithDescription(fmt.Sprintf("http url %s", hs.src.URL)))
- if err != nil {
- return nil, "", err
- }
- releaseRef := func() {
- newRef.Release(context.TODO())
- }
- defer func() {
- if retErr != nil && newRef != nil {
- releaseRef()
- }
- }()
- mount, err := newRef.Mount(ctx, false)
- if err != nil {
- return nil, "", err
- }
- lm := snapshot.LocalMounter(mount)
- dir, err := lm.Mount()
- if err != nil {
- return nil, "", err
- }
- defer func() {
- if retErr != nil && lm != nil {
- lm.Unmount()
- }
- }()
- perm := 0600
- if hs.src.Perm != 0 {
- perm = hs.src.Perm
- }
- fp := filepath.Join(dir, getFileName(hs.src.URL, hs.src.Filename, resp))
- f, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(perm))
- if err != nil {
- return nil, "", err
- }
- defer func() {
- if f != nil {
- f.Close()
- }
- }()
- h := sha256.New()
- if _, err := io.Copy(io.MultiWriter(f, h), resp.Body); err != nil {
- return nil, "", err
- }
- if err := f.Close(); err != nil {
- return nil, "", err
- }
- f = nil
- if hs.src.UID != 0 || hs.src.GID != 0 {
- if err := os.Chown(fp, hs.src.UID, hs.src.GID); err != nil {
- return nil, "", err
- }
- }
- mTime := time.Unix(0, 0)
- lastMod := resp.Header.Get("Last-Modified")
- if lastMod != "" {
- if parsedMTime, err := http.ParseTime(lastMod); err == nil {
- mTime = parsedMTime
- }
- }
- if err := os.Chtimes(fp, mTime, mTime); err != nil {
- return nil, "", err
- }
- lm.Unmount()
- lm = nil
- ref, err = newRef.Commit(ctx)
- if err != nil {
- return nil, "", err
- }
- newRef = nil
- hs.refID = ref.ID()
- dgst = digest.NewDigest(digest.SHA256, h)
- if respETag := resp.Header.Get("ETag"); respETag != "" {
- setETag(ref.Metadata(), respETag)
- uh, err := hs.urlHash()
- if err != nil {
- return nil, "", err
- }
- setChecksum(ref.Metadata(), uh.String(), dgst)
- if err := ref.Metadata().Commit(); err != nil {
- return nil, "", err
- }
- }
- if modTime := resp.Header.Get("Last-Modified"); modTime != "" {
- setModTime(ref.Metadata(), modTime)
- }
- return ref, dgst, nil
- }
- func (hs *httpSourceHandler) Snapshot(ctx context.Context) (cache.ImmutableRef, error) {
- if hs.refID != "" {
- ref, err := hs.cache.Get(ctx, hs.refID)
- if err == nil {
- return ref, nil
- }
- }
- req, err := http.NewRequest("GET", hs.src.URL, nil)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(ctx)
- resp, err := hs.client.Do(req)
- if err != nil {
- return nil, err
- }
- ref, dgst, err := hs.save(ctx, resp)
- if err != nil {
- return nil, err
- }
- if dgst != hs.cacheKey {
- ref.Release(context.TODO())
- return nil, errors.Errorf("digest mismatch %s: %s", dgst, hs.cacheKey)
- }
- return ref, nil
- }
- const keyETag = "etag"
- const keyChecksum = "http.checksum"
- const keyModTime = "http.modtime"
- func setETag(si *metadata.StorageItem, s string) error {
- v, err := metadata.NewValue(s)
- if err != nil {
- return errors.Wrap(err, "failed to create etag value")
- }
- si.Queue(func(b *bolt.Bucket) error {
- return si.SetValue(b, keyETag, v)
- })
- return nil
- }
- func getETag(si *metadata.StorageItem) string {
- v := si.Get(keyETag)
- if v == nil {
- return ""
- }
- var etag string
- if err := v.Unmarshal(&etag); err != nil {
- return ""
- }
- return etag
- }
- func setModTime(si *metadata.StorageItem, s string) error {
- v, err := metadata.NewValue(s)
- if err != nil {
- return errors.Wrap(err, "failed to create modtime value")
- }
- si.Queue(func(b *bolt.Bucket) error {
- return si.SetValue(b, keyModTime, v)
- })
- return nil
- }
- func getModTime(si *metadata.StorageItem) string {
- v := si.Get(keyModTime)
- if v == nil {
- return ""
- }
- var modTime string
- if err := v.Unmarshal(&modTime); err != nil {
- return ""
- }
- return modTime
- }
- func setChecksum(si *metadata.StorageItem, url string, d digest.Digest) error {
- v, err := metadata.NewValue(d)
- if err != nil {
- return errors.Wrap(err, "failed to create checksum value")
- }
- v.Index = url
- si.Queue(func(b *bolt.Bucket) error {
- return si.SetValue(b, keyChecksum, v)
- })
- return nil
- }
- func getChecksum(si *metadata.StorageItem) digest.Digest {
- v := si.Get(keyChecksum)
- if v == nil {
- return ""
- }
- var dgstStr string
- if err := v.Unmarshal(&dgstStr); err != nil {
- return ""
- }
- dgst, err := digest.Parse(dgstStr)
- if err != nil {
- return ""
- }
- return dgst
- }
- func getFileName(urlStr, manualFilename string, resp *http.Response) string {
- if manualFilename != "" {
- return manualFilename
- }
- if resp != nil {
- if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" {
- if _, params, err := mime.ParseMediaType(contentDisposition); err == nil {
- if params["filename"] != "" && !strings.HasSuffix(params["filename"], "/") {
- if filename := filepath.Base(filepath.FromSlash(params["filename"])); filename != "" {
- return filename
- }
- }
- }
- }
- }
- u, err := url.Parse(urlStr)
- if err == nil {
- if base := path.Base(u.Path); base != "." && base != "/" {
- return base
- }
- }
- return "download"
- }
|