-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtoken.go
179 lines (151 loc) · 7.27 KB
/
token.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
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"aidanwoods.dev/go-paseto"
)
type TokenProxy struct {
ServerConfig Config
SecretKey paseto.V4SymmetricKey
}
// // tokenProxyHandler proxies the token requests to the upstream token endpoints;
// // it adjusts the ?scope= parameter in the query from "repository:foo:..." to
// // "repository:repoPrefix/foo:.." and reverse proxies the query to the specified
// // tokenEndpoint.
// func tokenProxyHandler(config Config, tokenEndpoints map[string]string) http.HandlerFunc {
// // https://distribution.github.io/distribution/spec/auth/scope/
// // tokenProxyHandler: rewrote url:/_token?scope=repository%3Asnakeeyes%3Apull&service=registry.docker.io
// // into:https://auth.docker.io/token?scope=repository%3Abackplane%2Fsnakeeyes%3Apull&service=registry.docker.io
// // tokenProxyHandler: rewrote url:/_token?scope=repository%3Asnakeeyes%3Apull&service=registry.docker.io into:https://auth.docker.io/token?scope=repository%3Abackplane%2Fsnakeeyes%3Apull&service=registry.docker.io
// // tokenProxyHandler: rewrote url:/_token?scope=repository%3Apwgen%3Apull&service=registry.docker.io into:https://auth.docker.io/token?scope=repository%3Abackplane%2Fpwgen%3Apull&service=registry.docker.io
// // tokenProxyHandler: rewrote url:/_token?scope=repository%3Apwgen%3Apull&service=registry.docker.io into:https://auth.docker.io/token?scope=repository%3Abackplane%2Fpwgen%3Apull&service=registry.docker.io
// return (&httputil.ReverseProxy{
// FlushInterval: -1,
// Director: ,
// }).ServeHTTP
// }
// NewTokenProxy handles some things
func NewTokenProxy(cfg Config, secretKey paseto.V4SymmetricKey) http.HandlerFunc {
tp := &TokenProxy{
ServerConfig: cfg,
SecretKey: secretKey,
}
return (&httputil.ReverseProxy{
FlushInterval: -1,
Director: tp.Director,
Transport: tp,
}).ServeHTTP
}
// Director accepts requests at the local token endpoint and returns a
// re-written request to the upstream token service
func (tp *TokenProxy) Director(req *http.Request) {
originalURL := req.URL.String()
queryParams := req.URL.Query()
serviceParam := queryParams.Get("service")
if serviceParam == "" {
logger.Error("TokenProxy.Director: no service parameter was found in the request", "url", originalURL)
return
}
scopeParam := queryParams.Get("scope")
if scopeParam == "" {
logger.Error("TokenProxy.Director: no scope parameter was found in the request", "url", originalURL)
return
}
originalScope, err := ParseResourceScope(scopeParam)
if err != nil {
logger.Error("TokenProxy.Director: unable to parse request scope parameter", "error", err, "url", originalURL)
return
}
// we need to identify which of the config.ProxyItem members best matches
// the value in the orignalScope
proxy, err := tp.ServerConfig.BestMatch(originalScope)
if err != nil {
logger.Error("TokenProxy.Director: unable to match scope to a known proxy config", "scope", scopeParam, "error", err)
return
}
// update the host and set the service param
queryParams.Set("service", tokenEndpoints[proxy.RegistryHost].Service) // e.g. registry.docker.io
newScope, err := ParseResourceScope(scopeParam)
if err != nil {
panic("TokenProxy.Director: unable to parse scope param a second time, this shouldn't happen")
}
newScope.ResourceName = strings.Trim(fmt.Sprintf("%s/%s", proxy.RemotePrefix, strings.TrimPrefix(newScope.ResourceName, proxy.LocalPrefix)), "/")
queryParams.Set("scope", newScope.String())
logger.Debug("TokenProxy.Director: rewrote scope in request", "from", originalScope, "to", newScope)
// change the request from a request to our token endpoint to the remote token endpoint
u, _ := url.Parse(tokenEndpoints[proxy.RegistryHost].Realm) // e.g. https://auth.docker.io/token
u.RawQuery = queryParams.Encode()
req.Host = u.Host
req.URL = u
req.RequestURI = "" // clearing this to avoid conflicts
// add the proxy config key to the request context so the transport function can use it
req.Header.Set(proxyConfigHeader, proxy.LocalPrefix)
logger.Debug("TokenProxy.Director: rewrote url", "from", originalURL, "to", req.URL)
}
func (tp *TokenProxy) RoundTrip(req *http.Request) (*http.Response, error) {
logger.Debug("TokenProxy.RoundTrip: request received", "url", req.URL)
// Retrieve the proxy config value from the Director
proxyLocalPrefix := req.Header.Get(proxyConfigHeader)
if proxyLocalPrefix == "" {
return nil, fmt.Errorf("TokenProxy.RoundTrip: unable to get value in proxyConfigHeader %s", proxyConfigHeader)
}
req.Header.Del(proxyConfigHeader)
proxy, ok := tp.ServerConfig.Proxies[proxyLocalPrefix]
if !ok {
return nil, fmt.Errorf("TokenProxy.RoundTrip: unable to find key \"%s\" in cfg.Proxies", proxyLocalPrefix)
}
// at this point the docker client is requesting a token from us which can be used to download the image
// we don't require them to authenticate to us
authHeader := req.Header.Get("Authorization")
if authHeader != "" {
logger.Warn("TokenProxy.RoundTrip: WARNING received an Authorization header from the client", "header", authHeader)
}
req.Header.Set("Authorization", proxy.AuthHeader)
SetUserAgent(req, tp.ServerConfig.ProxyFQDN)
CleanHeaders(req)
LogRequest("TokenProxy.RoundTrip: about to send the following request to remote token service", req)
// make the request to the remote
resp, err := http.DefaultTransport.RoundTrip(req)
LogResponse("TokenProxy.RoundTrip: received the following response", resp)
if err != nil {
return nil, fmt.Errorf("TokenProxy.RoundTrip: upstream request failed with error: %+v", err)
}
logger.Debug("TokenProxy.RoundTrip: DEBUG upstream request completed", "status", resp.StatusCode, "url", req.URL)
// process the response body
responseData, err := ParseTokenRequestResponse(resp)
if err != nil {
return nil, fmt.Errorf("TokenProxy.RoundTrip: unable to parse upstream token response; err:%s", err)
}
logger.Debug("TokenProxy.RoundTrip: DEBUG parsed response", "data", responseData)
if responseData.Token == "" {
return nil, fmt.Errorf("TokenProxy.RoundTrip: no token found in parsed response body: %+v", responseData)
}
now := time.Now()
// we're going to need these to have a value for the calculation below
if responseData.IssuedAt.IsZero() {
responseData.IssuedAt = now
logger.Debug("TokenProxy.RoundTrip: DEBUG token from registry had no IssuedAt value (using computed value)", "IssuedAt", responseData.IssuedAt)
}
if responseData.ExpiresIn == 0 {
responseData.ExpiresIn = 600
logger.Debug("TokenProxy.RoundTrip: DEBUG token from registry had no ExpiresIn value (using computed value)", "seconds", responseData.ExpiresIn)
}
tokenExpiresAt := responseData.IssuedAt.Add(time.Duration(responseData.ExpiresIn) * time.Second)
// issue a token with the real upstream token embedded inside
token := paseto.NewToken()
token.SetIssuedAt(now)
token.SetNotBefore(now)
token.SetExpiration(tokenExpiresAt)
token.SetString(tokenKeyUpstreamToken, responseData.Token)
encryptedToken := token.V4Encrypt(tp.SecretKey, nil)
responseData.Token = encryptedToken
logger.Info("TokenProxy.RoundTrip: generted token", "claims", token.ClaimsJSON())
if err := ReplaceResponseBody(resp, responseData); err != nil {
return nil, fmt.Errorf("TokenProxy.RoundTrip: unable to update the response body: %s", err)
}
return resp, nil
}