不知道有多少人遇到我这个问题。当Docker的Registry服务器返回301、302 Redirect时,特定条件下,Docker客户端跟随返回的新地址时,认证信息会失效。导致后续请求都被视作匿名的未授权请求处理。
这是什么原因呢?这个特定条件又是指什么呢?
原来Docker客户端在做HTTP请求时,使用的是golang的默认http客户端。在golang的http客户端文档中,明确指出,golang在跟随重定向响应的新地址时,针对像Authorization
、WWW-Authenticate
和Cookie
这类HTTP Header,会有一个域安全性校验。如果新地址的域名和老地址不一样,并且不是老地址的子域名时,跟随请求会丢掉这些HTTP头,以保障请求安全性。
When following redirects, the Client will forward all headers set on the initial Request except: when forwarding sensitive headers like “Authorization”, “WWW-Authenticate”, and “Cookie” to untrusted targets. These headers will be ignored when following a redirect to a domain that is not a subdomain match or exact match of the initial domain. For example, a redirect from “foo.com” to either “foo.com” or “sub.foo.com” will forward the sensitive headers, but a redirect to “bar.com” will not.
这里提到了域和子域。那么针对域名满足条件,但是端口不满足条件的情况(比如从foo.com重定向到foo.com:8080),Go怎么处理的呢?我们跟进Go的源代码看看。相关代码位于net/http/client.go这个文件中,从Client.Do()
开始,依次调用copyHeaders()
=> makeHeadersCopier()
=> shouldCopyHeaderOnRedirect()
,我们来看看最后这个函数的源代码:
func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
switch CanonicalHeaderKey(headerKey) {
case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
// Permit sending auth/cookie headers from "foo.com"
// to "sub.foo.com".
// Note that we don't send all cookies to subdomains
// automatically. This function is only used for
// Cookies set explicitly on the initial outgoing
// client request. Cookies automatically added via the
// CookieJar mechanism continue to follow each
// cookie's scope as set by Set-Cookie. But for
// outgoing requests with the Cookie header set
// directly, we don't know their scope, so we assume
// it's for *.domain.com.
// TODO(bradfitz): once issue 16142 is fixed, make
// this code use those URL accessors, and consider
// "http://foo.com" and "http://foo.com:80" as
// equivalent?
// TODO(bradfitz): better hostname canonicalization,
// at least once we figure out IDNA/Punycode (issue
// 13835).
ihost := strings.ToLower(initial.Host)
dhost := strings.ToLower(dest.Host)
return isDomainOrSubdomain(dhost, ihost)
}
// All other headers are copied:
return true
}
// isDomainOrSubdomain reports whether sub is a subdomain (or exact
// match) of the parent domain.
//
// Both domains must already be in canonical form.
func isDomainOrSubdomain(sub, parent string) bool {
if sub == parent {
return true
}
// If sub is "foo.example.com" and parent is "example.com",
// that means sub must end in "."+parent.
// Do it without allocating.
if !strings.HasSuffix(sub, parent) {
return false
}
return sub[len(sub)-len(parent)-1] == '.'
}
从代码里可以看出,判断重定向时特殊的Header是否复制上,直接是通过Host字段字符串匹配进行的。而且代码中的TODO说的很清楚,针对带端口的请求,这里暂时没作处理。等#16142这个issue修复后再做处理。但是这里的处理,应该仅仅是针对默认的80端口,将带80端口和不带80端口的域视作同一个域。而其他端口则不作处理。
也就是说,如果你原始请求是test.com,新请求是test.com:8080,那么这个时候是不会作为同一个域处理,Header会丢弃。
如果原始请求是test.com,新请求是test.com:80。现阶段是会作为不同域处理,后续等issue修复后,这里会修复,修复后作为同域处理。
这里回到最开始我们说的Docker Registry的问题。如果你Registry地址返回的是301或者302重定向,而且重定向的新地址和Registry的地址不是同域名、同端口的话,那么Docker客户端在跟随新地址时,就会丢弃认证用的HTTP头,导致最终请求被视作匿名用户。
针对这个问题,我已经在Docker上提了Issue #865,不知Docker何时以何种方式处理。