-
Notifications
You must be signed in to change notification settings - Fork 801
/
auth.go
192 lines (158 loc) · 5.06 KB
/
auth.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package auth
import (
"encoding/json"
"errors"
"fmt"
"github.com/containrrr/watchtower/pkg/registry/helpers"
"github.com/containrrr/watchtower/pkg/types"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
// ChallengeHeader is the HTTP Header containing challenge instructions
const ChallengeHeader = "WWW-Authenticate"
// GetToken fetches a token for the registry hosting the provided image
func GetToken(container types.Container, registryAuth string) (string, error) {
var err error
var URL url.URL
if URL, err = GetChallengeURL(container.ImageName()); err != nil {
return "", err
}
logrus.WithField("URL", URL.String()).Debug("Building challenge URL")
var req *http.Request
if req, err = GetChallengeRequest(URL); err != nil {
return "", err
}
client := &http.Client{}
var res *http.Response
if res, err = client.Do(req); err != nil {
return "", err
}
defer res.Body.Close()
v := res.Header.Get(ChallengeHeader)
logrus.WithFields(logrus.Fields{
"status": res.Status,
"header": v,
}).Debug("Got response to challenge request")
challenge := strings.ToLower(v)
if strings.HasPrefix(challenge, "basic") {
if registryAuth == "" {
return "", fmt.Errorf("no credentials available")
}
return fmt.Sprintf("Basic %s", registryAuth), nil
}
if strings.HasPrefix(challenge, "bearer") {
return GetBearerHeader(challenge, container.ImageName(), err, registryAuth)
}
return "", errors.New("unsupported challenge type from registry")
}
// GetChallengeRequest creates a request for getting challenge instructions
func GetChallengeRequest(URL url.URL) (*http.Request, error) {
req, err := http.NewRequest("GET", URL.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "*/*")
req.Header.Set("User-Agent", "Watchtower (Docker)")
return req, nil
}
// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions
func GetBearerHeader(challenge string, img string, err error, registryAuth string) (string, error) {
client := http.Client{}
if strings.Contains(img, ":") {
img = strings.Split(img, ":")[0]
}
authURL, err := GetAuthURL(challenge, img)
if err != nil {
return "", err
}
var r *http.Request
if r, err = http.NewRequest("GET", authURL.String(), nil); err != nil {
return "", err
}
if registryAuth != "" {
logrus.Debug("Credentials found.")
logrus.Tracef("Credentials: %v", registryAuth)
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
} else {
logrus.Debug("No credentials found.")
}
var authResponse *http.Response
if authResponse, err = client.Do(r); err != nil {
return "", err
}
body, _ := ioutil.ReadAll(authResponse.Body)
tokenResponse := &types.TokenResponse{}
err = json.Unmarshal(body, tokenResponse)
if err != nil {
return "", err
}
return fmt.Sprintf("Bearer %s", tokenResponse.Token), nil
}
// GetAuthURL from the instructions in the challenge
func GetAuthURL(challenge string, img string) (*url.URL, error) {
loweredChallenge := strings.ToLower(challenge)
raw := strings.TrimPrefix(loweredChallenge, "bearer")
pairs := strings.Split(raw, ",")
values := make(map[string]string, len(pairs))
for _, pair := range pairs {
trimmed := strings.Trim(pair, " ")
kv := strings.Split(trimmed, "=")
key := kv[0]
val := strings.Trim(kv[1], "\"")
values[key] = val
}
logrus.WithFields(logrus.Fields{
"realm": values["realm"],
"service": values["service"],
}).Debug("Checking challenge header content")
if values["realm"] == "" || values["service"] == "" {
return nil, fmt.Errorf("challenge header did not include all values needed to construct an auth url")
}
authURL, _ := url.Parse(fmt.Sprintf("%s", values["realm"]))
q := authURL.Query()
q.Add("service", values["service"])
scopeImage := GetScopeFromImageName(img, values["service"])
scope := fmt.Sprintf("repository:%s:pull", scopeImage)
logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token")
q.Add("scope", scope)
authURL.RawQuery = q.Encode()
return authURL, nil
}
// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests
func GetScopeFromImageName(img, svc string) string {
parts := strings.Split(img, "/")
if len(parts) > 2 {
if strings.Contains(svc, "docker.io") {
return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/"))
}
return strings.Join(parts, "/")
}
if len(parts) == 2 {
if strings.Contains(parts[0], "docker.io") {
return fmt.Sprintf("library/%s", parts[1])
}
return strings.Replace(img, svc+"/", "", 1)
}
if strings.Contains(svc, "docker.io") {
return fmt.Sprintf("library/%s", parts[0])
}
return img
}
// GetChallengeURL creates a URL object based on the image info
func GetChallengeURL(img string) (url.URL, error) {
normalizedNamed, _ := reference.ParseNormalizedNamed(img)
host, err := helpers.NormalizeRegistry(normalizedNamed.String())
if err != nil {
return url.URL{}, err
}
URL := url.URL{
Scheme: "https",
Host: host,
Path: "/v2/",
}
return URL, nil
}