小坑惨寿,大跟頭邦泄。如何正確在 Golang 中在處理 Http Request 之前修改 Host 字段內(nèi)容

我是 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)容。

效果如下圖:

改進(jìn)流量模型

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)作青团。

獲得 Host 內(nèi)容流程

雖然在實(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)求的一致太伊。

修復(fù) Host 字段

結(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í)“避坑”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末淤齐,一起剝皮案震驚了整個(gè)濱河市束世,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌床玻,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沉帮,死亡現(xiàn)場(chǎng)離奇詭異锈死,居然都是意外死亡贫堰,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門待牵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)其屏,“玉大人,你說(shuō)我怎么就攤上這事缨该≠诵校” “怎么了?”我有些...
    開封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵贰拿,是天一觀的道長(zhǎng)蛤袒。 經(jīng)常有香客問(wèn)我,道長(zhǎng)膨更,這世上最難降的妖魔是什么妙真? 我笑而不...
    開封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮荚守,結(jié)果婚禮上珍德,老公的妹妹穿的比我還像新娘。我一直安慰自己矗漾,他們只是感情好锈候,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著敞贡,像睡著了一般泵琳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嫡锌,一...
    開封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天虑稼,我揣著相機(jī)與錄音,去河邊找鬼势木。 笑死蛛倦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的啦桌。 我是一名探鬼主播溯壶,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼甫男!你這毒婦竟也來(lái)了且改?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤板驳,失蹤者是張志新(化名)和其女友劉穎又跛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體若治,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡慨蓝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年感混,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片礼烈。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弧满,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出此熬,到底是詐尸還是另有隱情庭呜,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布犀忱,位于F島的核電站募谎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏峡碉。R本人自食惡果不足惜近哟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鲫寄。 院中可真熱鬧吉执,春花似錦、人聲如沸地来。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)未斑。三九已至咕宿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蜡秽,已是汗流浹背府阀。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留芽突,地道東北人试浙。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像寞蚌,于是被迫代替她去往敵國(guó)和親田巴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容