如何在Golang中正确的修改HTTPRequest的Host

背景

我们工作中经常需要通过HTTP请求Server的服务,比如脚本批量请求接口跑数据。在这个过程中,由于一些网关策略,部分Server会要求请求中Header里面附带Host参数。这时,我们可能会想到在Header里面直接赋值Host,比如这样:

go 复制代码
req.Header.Add("Host", "www.example.com")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

但请求的过程中会发现明明设置了,接收方却收不到这个Host。因此下面我会阐述为什么会这样,以及如何正确的修改HTTPRequest的Host。

为什么会这样

首先我们打印一下Header.Add后的参数看看会怎样

go 复制代码
map[Host:[www.example.com]]

再排除世界上有鬼的情况下,我们可以合理分析出: Header.Add环节既然成功了,那么Host一定是在实际请求HTTP前被替换了

由于下一段代码就是client.Do,因此有理由怀疑这个操作在Do方法里面

go 复制代码
// 发送请求并获取响应
resp, err := client.Do(req)
if err != nil {
	fmt.Println("发送请求失败:", err)
	return
}

追踪下去,可以看到net/http包的源码如下:

go 复制代码
func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}
func (c *Client) do(req *Request) (retres *Response, reterr error) {
    ... ... // 省略
    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
		}
	}
	ireq := reqs[0]
	req = &Request{
		Method:   redirectMethod,
		Response: resp,
		URL:      u,
		Header:   make(Header),
		Host:     host,
		Cancel:   ireq.Cancel,
		ctx:      ireq.ctx,
	}
    ... ... // 省略
}

这段指明:请求实际使用的Host默认为空。如果req.Host字段不为空,且不与URL的Host相同,会使用req.Host

go 复制代码
// If the caller specified a custom Host header and the redirect location is relative, preserve the Host header through the redirect.
// 如果调用者指定了自定义的 Host 标头并且重定向位置是相对路径的话,通过重定向保留该 Host 标头。

那么我们来看看req.Host字段是什么

go 复制代码
	// 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

这段注释主要解释了在 Go 语言中如何处理请求的 Host 标头。在服务器请求中,Host 指定要查找 URL 的主机,可能是 Host 标头的值或 URL 本身中给定的主机名。对于客户端请求,Host 可以选择性地覆盖要发送的 Host 标头,如果为空,则使用 URL.Host 的值。此外,还提到了国际化域名的处理和防止 DNS 重新绑定攻击的注意事项。

这基本跟我们上文的结论相互印证了,至此我们搞清楚了为什么Header里面的Host不生效:因为Do使用HTTP Request里面的Host字段,且不是Header里面的Host键对应值

怎么解决

显然,指明 req.Host 是一个较好的方案

go 复制代码
req.Host = "www.example.com"

至此我们解决了这个问题

我们还能知道些什么

关于 issue #22233

go 复制代码
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
    }
}

我们注意到在这段代码中,提到了issue #22233,那么它到底是什么呢,我们一起来看看!

这个问题 #issue 22233 是2017年由 timonwong 提出的,当时版本是 go1.9.1 darwin/amd64。问题内容是:客户端跟随重定向时不会保留 Host 标头 golang的一位维护者tombergan 响应了这个问题: 认为这绝对是个bug

但同时他也提出重定向时复制哪些header内容是没有一个较好的确切的指导的。

go 复制代码
Parent:     645c661a (cmd/compile/internal/syntax: factor out list parsing)
Author:     Tom Bergan <tombergan@google.com>
AuthorDate: 2017-10-13 15:56:37 -0700
Commit:     Tom Bergan <tombergan@google.com>
CommitDate: 2017-10-16 17:44:26 +0000
net/http: preserve Host header following a relative redirect
If the client sends a request with a custom Host header and receives
a relative redirect in response, the second request should use the
same Host header as the first request. However, if the response is
an abolute redirect, the Host header should not be preserved. See
further discussion on the issue tracker.
Fixes #22233
Change-Id: I8796e2fbc1c89b3445e651f739d5d0c82e727c14
Reviewed-on: https://go-review.googlesource.com/70792
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>

最终于2017-10-16这次提交中他修复了这个问题,从邮箱看这位大神应该是google的一位员工。代码改动如下:

至此我们了解了有关于 issue #22233 的全部

关于 Host 是什么

在前文中,Request的Host属性的注释中提到: Host指定了正在寻找的主机

go 复制代码
    // 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.

从这里面提到的 RFC 7230,section 5.4 可以看到

① Host提供目标URI的主机、端口信息,使服务器在单个IP地址上可以根据不同的主机名提供不同的服务和资源。(比如单机部署多个网站

② HTTP/1.1必须发送Host字段。当代理服务接受到absolute-form形式的请求时,忽略Host字段,取请求中的主机信息。转发请求的时候需要基于接收的请求重新生成Host,而不是转发接受到的Host。(URL里面的主机信息优先级高于Host字段。转发请求的时候Host字段不透传

③ Host本身可以任意修改,因此如果依赖Host字段进行代理转发、缓存密钥、身份验证等,需要先行校验Host值的合法性,避免Host头攻击(www.cnblogs.com/zhaijiahui/...

④ 对于缺少或者有多个Host字段的HTTP/1.1请求消息,服务器需要返回400 Bad Request 状态码 (Host字段有且只能有一个

至此,我们弄明白了Host是什么

go 复制代码
// example: www.example.org or www.example.org:8080
Host = uri-host [":" port]; 
相关推荐
2401_854391082 分钟前
Spring Boot OA:企业数字化转型的利器
java·spring boot·后端
山山而川粤9 分钟前
废品买卖回收管理系统|Java|SSM|Vue| 前后端分离
java·开发语言·后端·学习·mysql
2301_8112743111 分钟前
基于Spring Boot的同城宠物照看系统的设计与实现
spring boot·后端·宠物
2301_811274311 小时前
springboot嗨玩旅游网站
spring boot·后端·旅游
mit6.8242 小时前
[Redis#4] string | 常用命令 | + mysql use:cache | session
数据库·redis·后端·缓存
疯狂学习GIS2 小时前
创建第一个IDEA的Java项目的方法
java·后端·intellij idea
捂月2 小时前
Spring Boot 核心逻辑与工作原理详解
java·spring boot·后端
Nightselfhurt3 小时前
RPC学习
java·spring boot·后端·spring·rpc
Estar.Lee10 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
2401_8576100311 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全