Skip to content

Commit bd6820a

Browse files
authored
Merge pull request moby#5732 from marxarelli/feature/http-accept
http: Support additional request headers
2 parents 18db8b3 + e19af64 commit bd6820a

File tree

6 files changed

+121
-6
lines changed

6 files changed

+121
-6
lines changed

client/client_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
106106
testBuildHTTPSource,
107107
testBuildHTTPSourceEtagScope,
108108
testBuildHTTPSourceAuthHeaderSecret,
109+
testBuildHTTPSourceHeader,
109110
testBuildPushAndValidate,
110111
testBuildExportWithUncompressed,
111112
testBuildExportScratch,
@@ -3051,6 +3052,45 @@ func testBuildHTTPSourceAuthHeaderSecret(t *testing.T, sb integration.Sandbox) {
30513052
require.Equal(t, "Bearer foo", allReqs[0].Header.Get("Authorization"))
30523053
}
30533054

3055+
func testBuildHTTPSourceHeader(t *testing.T, sb integration.Sandbox) {
3056+
c, err := New(sb.Context(), sb.Address())
3057+
require.NoError(t, err)
3058+
defer c.Close()
3059+
3060+
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
3061+
3062+
resp := httpserver.Response{
3063+
Etag: identity.NewID(),
3064+
Content: []byte("content1"),
3065+
LastModified: &modTime,
3066+
}
3067+
3068+
server := httpserver.NewTestServer(map[string]httpserver.Response{
3069+
"/foo": resp,
3070+
})
3071+
defer server.Close()
3072+
3073+
st := llb.HTTP(
3074+
server.URL+"/foo",
3075+
llb.Header(llb.HTTPHeader{
3076+
Accept: "application/vnd.foo",
3077+
UserAgent: "fooagent",
3078+
}),
3079+
)
3080+
3081+
def, err := st.Marshal(sb.Context())
3082+
require.NoError(t, err)
3083+
3084+
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
3085+
require.NoError(t, err)
3086+
3087+
allReqs := server.Stats("/foo").Requests
3088+
require.Equal(t, 1, len(allReqs))
3089+
require.Equal(t, http.MethodGet, allReqs[0].Method)
3090+
require.Equal(t, "application/vnd.foo", allReqs[0].Header.Get("accept"))
3091+
require.Equal(t, "fooagent", allReqs[0].Header.Get("user-agent"))
3092+
}
3093+
30543094
func testResolveAndHosts(t *testing.T, sb integration.Sandbox) {
30553095
requiresLinux(t)
30563096
c, err := New(sb.Context(), sb.Address())

client/llb/source.go

+32
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,10 @@ func HTTP(url string, opts ...HTTPOption) State {
615615
attrs[pb.AttrHTTPAuthHeaderSecret] = hi.AuthHeaderSecret
616616
addCap(&hi.Constraints, pb.CapSourceHTTPAuth)
617617
}
618+
if hi.Header != nil {
619+
hi.Header.setAttrs(attrs)
620+
addCap(&hi.Constraints, pb.CapSourceHTTPHeader)
621+
}
618622

619623
addCap(&hi.Constraints, pb.CapSourceHTTP)
620624
source := NewSource(url, attrs, hi.Constraints)
@@ -629,6 +633,7 @@ type HTTPInfo struct {
629633
UID int
630634
GID int
631635
AuthHeaderSecret string
636+
Header *HTTPHeader
632637
}
633638

634639
type HTTPOption interface {
@@ -666,6 +671,33 @@ func Chown(uid, gid int) HTTPOption {
666671
})
667672
}
668673

674+
// Header returns an [HTTPOption] that ensures additional request headers will
675+
// be sent when retrieving the HTTP source.
676+
func Header(header HTTPHeader) HTTPOption {
677+
return httpOptionFunc(func(hi *HTTPInfo) {
678+
hi.Header = &header
679+
})
680+
}
681+
682+
type HTTPHeader struct {
683+
Accept string
684+
UserAgent string
685+
}
686+
687+
func (hh *HTTPHeader) setAttrs(attrs map[string]string) {
688+
if hh.Accept != "" {
689+
attrs[hh.attr("accept")] = hh.Accept
690+
}
691+
692+
if hh.UserAgent != "" {
693+
attrs[hh.attr("user-agent")] = hh.UserAgent
694+
}
695+
}
696+
697+
func (hh *HTTPHeader) attr(name string) string {
698+
return pb.AttrHTTPHeaderPrefix + name
699+
}
700+
669701
func platformSpecificSource(id string) bool {
670702
return strings.HasPrefix(id, "docker-image://") || strings.HasPrefix(id, "oci-layout://")
671703
}

solver/pb/attr.go

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const AttrHTTPPerm = "http.perm"
2121
const AttrHTTPUID = "http.uid"
2222
const AttrHTTPGID = "http.gid"
2323
const AttrHTTPAuthHeaderSecret = "http.authheadersecret"
24+
const AttrHTTPHeaderPrefix = "http.header."
2425

2526
const AttrImageResolveMode = "image.resolvemode"
2627
const AttrImageResolveModeDefault = "default"

solver/pb/caps.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const (
3636
CapSourceHTTPPerm apicaps.CapID = "source.http.perm"
3737
// NOTE the historical typo
3838
CapSourceHTTPUIDGID apicaps.CapID = "soruce.http.uidgid"
39+
CapSourceHTTPHeader apicaps.CapID = "source.http.header"
3940

4041
CapSourceOCILayout apicaps.CapID = "source.ocilayout"
4142

@@ -238,13 +239,19 @@ func init() {
238239
})
239240

240241
Caps.Init(apicaps.Cap{
241-
ID: CapSourceOCILayout,
242+
ID: CapSourceHTTPUIDGID,
242243
Enabled: true,
243244
Status: apicaps.CapStatusExperimental,
244245
})
245246

246247
Caps.Init(apicaps.Cap{
247-
ID: CapSourceHTTPUIDGID,
248+
ID: CapSourceHTTPHeader,
249+
Enabled: true,
250+
Status: apicaps.CapStatusExperimental,
251+
})
252+
253+
Caps.Init(apicaps.Cap{
254+
ID: CapSourceOCILayout,
248255
Enabled: true,
249256
Status: apicaps.CapStatusExperimental,
250257
})

source/http/identifier.go

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type HTTPIdentifier struct {
2626
UID int
2727
GID int
2828
AuthHeaderSecret string
29+
Header []HeaderField
30+
}
31+
32+
type HeaderField struct {
33+
Name string
34+
Value string
2935
}
3036

3137
var _ source.Identifier = (*HTTPIdentifier)(nil)

source/http/source.go

+33-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package http
22

33
import (
44
"bytes"
5+
"cmp"
56
"context"
67
"crypto/sha256"
78
"encoding/json"
@@ -13,6 +14,7 @@ import (
1314
"os"
1415
"path"
1516
"path/filepath"
17+
"slices"
1618
"strconv"
1719
"strings"
1820
"time"
@@ -33,6 +35,13 @@ import (
3335
"github.com/pkg/errors"
3436
)
3537

38+
// supportedUserHeaders defines supported user-defined header fields. Fields
39+
// not included here will be silently dropped.
40+
var supportedUserDefinedHeaders = map[string]bool{
41+
http.CanonicalHeaderKey("accept"): true,
42+
http.CanonicalHeaderKey("user-agent"): true,
43+
}
44+
3645
type Opt struct {
3746
CacheAccessor cache.Accessor
3847
Transport http.RoundTripper
@@ -95,9 +104,22 @@ func (hs *httpSource) Identifier(scheme, ref string, attrs map[string]string, pl
95104
id.GID = int(i)
96105
case pb.AttrHTTPAuthHeaderSecret:
97106
id.AuthHeaderSecret = v
107+
default:
108+
if name, found := strings.CutPrefix(k, pb.AttrHTTPHeaderPrefix); found {
109+
name = http.CanonicalHeaderKey(name)
110+
if supportedUserDefinedHeaders[name] {
111+
id.Header = append(id.Header, HeaderField{Name: name, Value: v})
112+
}
113+
}
98114
}
99115
}
100116

117+
// Sort header fields to ensure consistent hashing (see urlHash() and
118+
// formatCacheKey())
119+
slices.SortFunc(id.Header, func(a, b HeaderField) int {
120+
return cmp.Compare(a.Name, b.Name)
121+
})
122+
101123
return id, nil
102124
}
103125

@@ -133,6 +155,7 @@ func (hs *httpSourceHandler) urlHash() (digest.Digest, error) {
133155
Filename []byte
134156
Perm, UID, GID int
135157
AuthHeaderSecret string `json:",omitempty"`
158+
Header []HeaderField
136159
}{
137160
Filename: bytes.Join([][]byte{
138161
[]byte(hs.src.URL),
@@ -142,6 +165,7 @@ func (hs *httpSourceHandler) urlHash() (digest.Digest, error) {
142165
UID: hs.src.UID,
143166
GID: hs.src.GID,
144167
AuthHeaderSecret: hs.src.AuthHeaderSecret,
168+
Header: hs.src.Header,
145169
})
146170
if err != nil {
147171
return "", err
@@ -154,8 +178,9 @@ func (hs *httpSourceHandler) formatCacheKey(filename string, dgst digest.Digest,
154178
Filename string
155179
Perm, UID, GID int
156180
Checksum digest.Digest
157-
LastModTime string `json:",omitempty"`
158-
AuthHeaderSecret string `json:",omitempty"`
181+
LastModTime string `json:",omitempty"`
182+
AuthHeaderSecret string `json:",omitempty"`
183+
Header []HeaderField `json:",omitempty"`
159184
}{
160185
Filename: filename,
161186
Perm: hs.src.Perm,
@@ -164,6 +189,7 @@ func (hs *httpSourceHandler) formatCacheKey(filename string, dgst digest.Digest,
164189
Checksum: dgst,
165190
LastModTime: lastModTime,
166191
AuthHeaderSecret: hs.src.AuthHeaderSecret,
192+
Header: hs.src.Header,
167193
})
168194
if err != nil {
169195
return dgst
@@ -219,7 +245,7 @@ func (hs *httpSourceHandler) CacheKey(ctx context.Context, g session.Group, inde
219245
for t := range m {
220246
etags = append(etags, t)
221247
}
222-
req.Header.Add("If-None-Match", strings.Join(etags, ", "))
248+
req.Header.Set("If-None-Match", strings.Join(etags, ", "))
223249

224250
if len(etags) == 1 {
225251
onlyETag = etags[0]
@@ -236,7 +262,7 @@ func (hs *httpSourceHandler) CacheKey(ctx context.Context, g session.Group, inde
236262
req.Method = "HEAD"
237263
// we need to add accept-encoding header manually because stdlib only adds it to GET requests
238264
// some servers will return different etags if Accept-Encoding header is different
239-
req.Header.Add("Accept-Encoding", "gzip")
265+
req.Header.Set("Accept-Encoding", "gzip")
240266
resp, err := client.Do(req)
241267
if err == nil {
242268
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotModified {
@@ -482,6 +508,9 @@ func (hs *httpSourceHandler) newHTTPRequest(ctx context.Context, g session.Group
482508
}
483509

484510
req.Header.Set("User-Agent", version.UserAgent())
511+
for _, field := range hs.src.Header {
512+
req.Header.Set(field.Name, field.Value)
513+
}
485514

486515
if hs.src.AuthHeaderSecret != "" {
487516
err := hs.sm.Any(ctx, g, func(ctx context.Context, _ string, caller session.Caller) error {

0 commit comments

Comments
 (0)