我是 LEE,老李裂垦,一個(gè)在 IT 行業(yè)摸爬滾打 17 年的技術(shù)老兵顺囊。
事件背景
前幾天公司內(nèi)部的 Serverless 項(xiàng)目技術(shù)定審核和啟動(dòng)會(huì)剛開完,上游的一個(gè)事業(yè)群的負(fù)責(zé)人煞有其事的找到我說(shuō):老李蕉拢,你上次開啟動(dòng)會(huì)說(shuō)特碳,Request 的 Header 頭中的 Host 字段因?yàn)槁酚蓵?huì)被修改,然后你們使用了 X-Forward-Host 來(lái)代替晕换。這幾天我跟其他幾個(gè)事業(yè)群的小伙伴溝通的時(shí)候發(fā)現(xiàn)午乓,業(yè)務(wù)側(cè)很多人都依賴 Request 中的 Host 字段,而且需要多一步判斷動(dòng)作闸准,太多項(xiàng)目要修改了益愈。你看你們有沒有辦法完善下。一者你們做底層架構(gòu)夷家,統(tǒng)一修改遠(yuǎn)比我們這么多項(xiàng)目都改成本小蒸其。二者你們提供的解決方案,我們直接使用也比較放心啊库快。
在通過(guò)一番了解后摸袁,發(fā)現(xiàn)小伙伴所言不虛。為了業(yè)務(wù)線全面遷移到 Serverless 系統(tǒng)上缺谴,要對(duì)這么多的業(yè)務(wù)系統(tǒng)代碼都需要改造但惶,確實(shí)不太合理耳鸯。而且每一個(gè)事業(yè)群下的不同業(yè)務(wù)研發(fā)都有自己的想法,難以一時(shí)統(tǒng)一膀曾。糾結(jié)一段時(shí)間后县爬,決定在我們部門成立一個(gè)小組專攻這個(gè)問(wèn)題。
最終的目標(biāo):業(yè)務(wù)線在遷移到 Serverless 系統(tǒng)中添谊,跟使用原有 Kubernetes 系統(tǒng)中 Ingress 邏輯一樣财喳。
心智負(fù)擔(dān)
在往下談的之前,我先說(shuō)說(shuō)為什么小伙伴們的“心智負(fù)擔(dān)”是什么斩狱?這樣讓大家能夠了解耳高,我們碰到這個(gè)問(wèn)題為什么要這么解決。
此前我們先看看所踊,最早一版本 結(jié)構(gòu)簡(jiǎn)圖:
通過(guò)上圖泌枪,我們可以得知,Knative Ingress 需要轉(zhuǎn)發(fā)流量到 Istio IngressGateway 上需要修改 Host 頭部字段秕岛,才能滿足 Istio IngressGateway 將 Http 請(qǐng)求轉(zhuǎn)發(fā)到后端的 Kubernetes 的 Pod 上碌燕。所以會(huì)導(dǎo)致后端的 Kubernetes 的 Pod 中讀取 Http Request 的 Header 中 Host 字段并不是真正客戶 HTTP 請(qǐng)求端傳入的 Host 字段
為了能夠?qū)?Http Request 的原始 Host 往后傳,我們決定使用 X-Forwarded-Host 作為“載體”继薛,將 Host 原始內(nèi)容傳入到后端 Pod修壕。這樣應(yīng)用就能夠讓 Pod 收到真實(shí) Host 內(nèi)容。
效果如下圖:
TIPS: 實(shí)際就是在 Knative Ingress 上開發(fā)了一個(gè)插件來(lái)實(shí)現(xiàn)效果遏考。
當(dāng)然在進(jìn)入 Pod 之前慈鸠,也要用另外一個(gè)插件來(lái)解決將 X-Forwarded-Host 轉(zhuǎn)換回 Host 字段,要不然所有小伙伴在處理業(yè)務(wù)請(qǐng)求的時(shí)候灌具,需要額外做如下的動(dòng)作青团。
雖然在實(shí)際效果上一定程度上解決了問(wèn)題,但是從某種意義上說(shuō)咖楣,我們只是迂回解決了問(wèn)題壶冒,沒有從本質(zhì)上解決。這才導(dǎo)致之前事業(yè)群的負(fù)責(zé)人找到我們截歉,希望我這邊提供解決方案,并完美的解決問(wèn)題烟零。
顯然其他事業(yè)群的小伙伴不愿意對(duì)原有系統(tǒng)和業(yè)務(wù)代碼做過(guò)多的流程改造瘪松。所以只能我們另辟蹊徑,在 Pod 前的 Sidecar 中將 Host 轉(zhuǎn)換回來(lái)锨阿。
真正的問(wèn)題
我們?cè)谛扪a(bǔ) Sidecar 的代碼還算順利宵睦,最重要的工作主要放在了 Http Request 中 Header 字段處理上。一段時(shí)間過(guò)去了以后墅诡,我去追進(jìn)度壳嚎,一個(gè)研發(fā)小伙伴反饋,X-Forwarded-Host 轉(zhuǎn)換回 Host 沒辦法實(shí)現(xiàn),而且代碼正確烟馅,debug 看了就是沒有辦法回寫说庭。 我覺得很奇怪,怎么會(huì)沒有辦法讓 X-Forwarded-Host 轉(zhuǎn)換回 Host 呢郑趁?
問(wèn)題定位
經(jīng)過(guò)一段時(shí)間代碼排查刊驴,咋一看小伙伴寫的代碼確實(shí)沒有問(wèn)題,而且也符合邏輯寡润。為什么就是不能實(shí)現(xiàn)效果呢捆憎?
xfh := request.Header.Get("X-Forwarded-Host")
request.Header.Set("Host", strings.Trimspace(xfh))
我自己獨(dú)立拉去了代碼,仔細(xì)往下看梭纹,才發(fā)現(xiàn)問(wèn)題所在躲惰。并不是這個(gè)小伙伴開發(fā)有問(wèn)題,而是對(duì) golang 的 net/http 包的內(nèi)部處理流程不熟悉变抽,想當(dāng)然的開發(fā)础拨。
最后我就改了他一行代碼就實(shí)現(xiàn)了效果。
// request.Header.Set("Host", strings.Trimspace(xfh))
request.Host = strings.Trimspace(xfh)
再對(duì)測(cè)試模型發(fā)起 Http 請(qǐng)求瞬沦,觀察模擬業(yè)務(wù) Pod 收到的 Http Request 中 Host 是否與最初請(qǐng)求的一致太伊。
結(jié)果如我所料,符合整個(gè)預(yù)期逛钻。
問(wèn)題模擬與再現(xiàn)
說(shuō)到這里僚焦,雖然我已經(jīng)把問(wèn)題解決了,讓項(xiàng)目繼續(xù)往前走曙痘,而且在周五下班前的第一版的內(nèi)測(cè)芳悲,反饋效果良好。但是我覺還是非常有必要講清楚這個(gè)問(wèn)題边坤,因?yàn)槲蚁脒@個(gè)小問(wèn)題非常容易“踩坑”名扛,而且會(huì)長(zhǎng)時(shí)間的照不到解決方案。
這里我用 Demo 代碼做演示茧痒,重要的是說(shuō)明問(wèn)題所在肮韧。 模擬一個(gè) Http 請(qǐng)求,在發(fā)送請(qǐng)求的前旺订,修改 Request Host 字段弄企。
func DoRequest(url string) (*http.Response, error) {
client := &http.Client{}
request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// request.Header.Set("Host", "specific-host") //錯(cuò)誤方式
request.Host = "specific-host"
rsp, err := client.Do(request)
if err != nil {
return nil, err
}
return rsp, nil
}
通過(guò)實(shí)際測(cè)試,會(huì)發(fā)現(xiàn)兩種寫法有截然不同的兩種結(jié)果区拳。
request.Header.Set("Host", "specific-host")
// 響應(yīng)結(jié)果:get request 127.0.0.1:40280 to 127.0.0.1:11010
// 不符合預(yù)期
request.Host = "specific-host"
// 響應(yīng)結(jié)果:get request 127.0.0.1:40293 to specific-host
// 符合預(yù)期
看到這里拘领,應(yīng)該有小伙伴應(yīng)該會(huì)有一愣,只怕是寫很多年的 Golang 代碼樱调,實(shí)現(xiàn)了無(wú)數(shù)的處理 Http Request 邏輯约素,這里提到的問(wèn)題沒有怎么注意過(guò)吧届良? 哈哈哈,實(shí)際我這里碰到這樣的問(wèn)題也是少有的幾次圣猎。
為了真正的能解釋問(wèn)題士葫,我們繼續(xù)往下追代碼。
go/src/net/http/client.go
// 包裝函數(shù)
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
// 真正的處理函數(shù)
func (c *Client) do(req *Request) (retres *Response, reterr error) {
...
for {
// For all but the first request, create the next
// request hop and replace req.
if len(reqs) > 0 {
...
// 處理 Http Host
host := ""
if req.Host != "" && req.Host != req.URL.Host {
// If the caller specified a custom Host header and the
// redirect location is relative, preserve the Host header
// through the redirect. See issue #22233.
if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
host = req.Host
}
}
...
// 構(gòu)造 Http Request
req = &Request{
Method: redirectMethod,
Response: resp,
URL: u,
Header: make(Header), // 構(gòu)造 Header样漆,request.Header.Set("Host", "specific-host") 在這里
Host: host, // 傳入 Host, request.Host = "specific-host" 在這里
Cancel: ireq.Cancel,
ctx: ireq.ctx,
}
...
// 發(fā)送 Http 請(qǐng)求
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// c.send() always closes req.Body
reqBodyClosed = true
if !deadline.IsZero() && didTimeout() {
err = &httpError{
err: err.Error() + " (Client.Timeout exceeded while awaiting headers)",
timeout: true,
}
}
return nil, uerr(err)
}
...
req.closeBody()
}
}
看到這里就差不多明白了为障,要在處理 Http Request 的請(qǐng)求之前要對(duì) Host 字段修改,就要使用 Request.Host 而不是 Header.Set("Host")放祟。這個(gè)邏輯在 Http Server 中也是一樣鳍怨,gin 框架底層就是使用 Http Server,所以也是這個(gè)處理邏輯跪妥。
說(shuō)了這么多鞋喇,真的說(shuō)明白了嗎?就要使用 Request.Host 而不是 Header.Set("Host") 嗎眉撵?如果你覺得是侦香,那我得說(shuō),那不是我老李的風(fēng)格纽疟,咱們要繼續(xù)一定要找到官方說(shuō)明罐韩。
下面是 golang 源代碼中對(duì) Http Request 的定義和說(shuō)明,我們仔細(xì)看下面的注釋污朽。
// A Request represents an HTTP request received by a server
// or to be sent by a client.
//
// The field semantics differ slightly between client and server
// usage. In addition to the notes on the fields below, see the
// documentation for Request.Write and RoundTripper.
type Request struct {
...
// Header contains the request header fields either received
// by the server or to be sent by the client.
//
// If a server received a request with header lines,
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
//
// then
//
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Foo": {"Bar", "two"},
// }
//
// For incoming requests, the Host header is promoted to the
// Request.Host field and removed from the Header map.
//
// HTTP defines that header names are case-insensitive. The
// request parser implements this by using CanonicalHeaderKey,
// making the first character and any characters following a
// hyphen uppercase and the rest lowercase.
//
// For client requests, certain headers such as Content-Length
// and Connection are automatically written when needed and
// values in Header may be ignored. See the documentation
// for the Request.Write method.
Header Header
...
// For server requests, Host specifies the host on which the
// URL is sought. For HTTP/1 (per RFC 7230, section 5.4), this
// is either the value of the "Host" header or the host name
// given in the URL itself. For HTTP/2, it is the value of the
// ":authority" pseudo-header field.
// It may be of the form "host:port". For international domain
// names, Host may be in Punycode or Unicode form. Use
// golang.org/x/net/idna to convert it to either format if
// needed.
// To prevent DNS rebinding attacks, server Handlers should
// validate that the Host header has a value for which the
// Handler considers itself authoritative. The included
// ServeMux supports patterns registered to particular host
// names and thus protects its registered Handlers.
//
// For client requests, Host optionally overrides the Host
// header to send. If empty, the Request.Write method uses
// the value of URL.Host. Host may contain an international
// domain name.
Host string
...
}
golang 官方就特意把 Host 字段單獨(dú)從 Request Header 中拿出來(lái)處理散吵。
我想其中一個(gè)比較重要的原因可能是這個(gè):
For client requests, Host optionally overrides the Host header to send. If empty, the Request.Write method uses the value of URL.Host. Host may contain an international domain name.
說(shuō)到最后
雖然修改 Http Request 中 Host 字段內(nèi)容不是什么了不起的方法,正因?yàn)槭沁@樣導(dǎo)致我們?cè)谶@塊儲(chǔ)備不足蟆肆,導(dǎo)致在實(shí)際工作中碰到這個(gè)卡脖子的問(wèn)題矾睦,導(dǎo)致長(zhǎng)時(shí)間沒有解決。而且我 Google 了下炎功,沒有什么人對(duì)這個(gè)問(wèn)題做了詳細(xì)解釋枚冗,所以最終決定將這個(gè)問(wèn)題“解法”成文,并放在網(wǎng)絡(luò)上蛇损,供大家參考赁温,能夠幫助大家及時(shí)“避坑”。