写点什么

有趣!一行代码居然无法获取请求的完整 URL

用户头像
Gopher指北
关注
发布于: 2021 年 03 月 31 日
有趣!一行代码居然无法获取请求的完整URL

来自公众号:Gopher指北

缘起


做 Web 服务的时候,可能会有这样一个业务场景,获取一个 HTTP 请求的完整 URL。很巧,老许就碰到了这样的业务场景。面对如此简单的需求,CV 大法根本没有展示才能的机会。啪啪啪,获取请求的完整 URL 代码就出来了。


当时离验证只差一步,老许信心满满,很快,打脸来得很快就像龙卷风。。。


从图中可以知道,req.URL中的SchemeHost均为空,所以r.URL.String()无法得到完整的请求连接。这个结果让老许一阵激动,万万没想到有一天我也有机会发现 Go 源码中可能遗漏的赋值。老许强行按耐住心中的激动,准备好好研究一番,万一成为了 Go 的 Contributor 呢\^ω\^。最后发现官方实现没有问题,因此就有了今天这篇文章。

HTTP1.1 中为什么无法获取完整的连接

HTTP1.1 的 Server 读取请求并构建Request.URL对象的逻辑在 request.go 文件的readRequest方法中,下面老许对其源码做一个简单分析总结。

  1. 读取请求的第一行,HTTP 请求的第一行又称为请求行。


// First line: GET /index.html HTTP/1.0var s stringif s, err = tp.ReadLine(); err != nil {	return nil, err}
复制代码


  1. 将请求行的内容分别解析为req.Methodreq.RequestURIreq.Proto

var ok boolreq.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
复制代码


  1. req.RequestURI解析为req.URL


rawurl := req.RequestURIif req.URL, err = url.ParseRequestURI(rawurl); err != nil {	return nil, err}
复制代码


注:当请求方法是 CONNECT 时,上述流程略有变化


通过上面的流程我们知道req.URL的数据来源为req.RequestURI,而req.RequestURI到底是什么让我们继续阅读后文。

请求资源

根据 rfc7230 中的定义, 请求行分为请求方法、请求资源和 HTTP 版本,分别对应上述的req.Methodreq.RequestURIreq.Proto(request-target 在本文均被译作请求资源)。


关于请求方法有哪些想必不用老许在这儿科普了吧。至于常用的 HTTP 版本无非就是 HTTP1.1 和 HTTP2。 下面主要介绍请求资源的几种形式。

origin-form

这种形式是请求资源中最常见的形式,其格式定义如下。

origin-form    = absolute-path [ "?" query ]
复制代码


当直接向服务器发起请求时,除开 CONNECT 和 OPTIONS 请求,只允许发送 path 和 query 作为请求资源。如果请求链接的 path 为空,则必须发送/作为请求资源。请求链接中的 Host 信息以 Header 头的形式发送。


http://www.example.org/where?q=now为例,请求行和 Host 请求头信息如下


GET /where?q=now HTTP/1.1Host: www.example.org
复制代码

absolute-form

这种形式目前仅在向代理发起请求时使用,其格式定义如下。

absolute-form  = absolute-URI
复制代码


根据 rfc7230 中的定义,目前 client 仅会向代理发送这种形式的请求资源,但为了将来某个 HTTP 版本可能会转换为这种形式的请求资源所以 server 需要支持这种形式的请求资源。这大概就是为什么req.URL中大部分字段值为空却仍然将 URL 各部分定义完整的原因。


一个absolute-form形式的请求行例子如下。

GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1
复制代码


authority-form

authority-form形式的请求资源仅用于CONNECT请求中,其格式定义如下。


authority-form = authority
复制代码

发送CONNECT请求时,client 只能发送 URI 的 authority 部分(不包含 userinfo 和 @定界符)作为请求资源。这样讲比较抽象, 我们先来看看http-URI的定义。


通过上面这张图大概能够猜出来authority应该是指 Host 信息。Very Good!你没有猜错!


The origin server for an "http" URI is identified by the authority component, which includes a host identifier and optional TCP port.
复制代码

上面是 rfc7230 对于 authority 的解释。老许根据自己的翻译,在这里单方面宣布authority包括主机标识符和可选的端口信息。一个authority-form形式的请求行例子如下。


CONNECT www.example.com:80 HTTP/1.1
复制代码


asterisk-form

asterisk-form形式的请求资源仅适用于OPTIONS请求且只能为*,其格式定义如下。


asterisk-form  = "*"
复制代码

一个asterisk-form形式的请求行例子如下。


OPTIONS * HTTP/1.1
复制代码


对上面几种形式的请求资源有所了解后,我们再次回到获取请求的完整 URL 这一问题本身。以最常用的absolute-form为例(其他形式的请求资源我们在开发中几乎不用考虑),请求资源中本身就缺少HostScheme信息,所以一行代码自然无法获取请求的完整 URL。难道我们就无法获取到请求的完整 URL 嘛?当然不是,我们还可以通过以下两种方案得到完整的 URL。


方案一

  1. 通过req.Host得到 Host 相关信息。

  2. 如果req.TLS == nil则为 HTTP 请求,否则为 HTTPS 请求。

  3. 通过步骤 1、步骤 2 并结合请求行信息即可得到完整的 URL。


方案二

在配置文件中配置好服务的 Host 信息,获取完整请求时只需要读取配置文件并拼接req.RequestURI即可。事实上老许采用的就是方案二,因为很多服务都在网关后面。当客户端使用 HTTPS 请求网关,网关以 HTTP 请求服务时使用req.TLS == nil判断就不合理了。

HTTP2 中为什么无法获取完整的连接

需要注意的是在 HTTP2 中已经没有请求行的概念了,取而代之的是请求伪标头,这一点老许在Go发起HTTP2.0请求流程分析(后篇)——标头压缩这篇文章中提到过。

下图为一次 HTTP2 请求的部分 Header 信息。


从图中可以发现,HTTP1.1 中的请求行已经没有了。根据 rfc7540 中的定义,请求的伪标头字段有:method:scheme:authority:path


:method:scheme不需要老许多说,看英文单词的意思就可以了。


:authority: 根据前文的解释,其值为主机标识符和可选的端口信息。另外需要注意的是 HTTP2 中没有Host请求头。


:path: 如果是OPTIONS请求,则其值为*。其他情况该值为请求 URI 的 path 和 query,如果 path 为空则其值为/


在对 HTTP2 请求的伪标头有了一个基本了解后,下面我们来看一下Request.URL的赋值过程。HTTP2 的 Server 读取请求并构建Request.URL对象的逻辑在 h2_bundle.go 文件的(*http2serverConn).newWriterAndRequestNoBody方法中。


  1. 如果是CONNECT请求通过:authority构建url_,否则通过:path构建url_

if rp.method == "CONNECT" {	url_ = &url.URL{Host: rp.authority}	requestURI = rp.authority // mimic HTTP/1 server behavior} else {	var err error	url_, err = url.ParseRequestURI(rp.path)	if err != nil {		return nil, nil, http2streamError(st.id, http2ErrCodeProtocol)	}	requestURI = rp.path}
复制代码


  1. url_赋值给req.URL


req := &Request{	Method:     rp.method,	URL:        url_,	RemoteAddr: sc.remoteAddrStr,	Header:     rp.header,	RequestURI: requestURI,	Proto:      "HTTP/2.0",	ProtoMajor: 2,	ProtoMinor: 0,	TLS:        tlsState,	Host:       rp.authority,	Body:       body,	Trailer:    trailer,}
复制代码

由于:path标头的值也不包含 Host 信息,所以 HTTP2 的 server 也无法通过req.URL.String()得到请求的完整 URL。


在这里我们反思一个问题。通过伪标头字段已经能够得到完整的 URL,为什么仍然只读取:path:authority中的一个来赋值req.URL呢?


老许在这里猜测可能原因是希望开发者无需关心请求是 HTTP1.1 还是 HTTP2,避免不必要的 HTTP 版本判断。


关于获取请求完整 URL 的思考就到这里。最后,衷心希望本文能够对各位读者有一定的帮助。


1. 写本文时, 笔者所用 go 版本为: go1.15.2


参考:


https://tools.ietf.org/html/rfc7230


https://tools.ietf.org/html/rfc7540


发布于: 2021 年 03 月 31 日阅读数: 8
用户头像

Gopher指北

关注

还未添加个人签名 2020.09.15 加入

欢迎关注公众号:Gopher指北

评论

发布
暂无评论
有趣!一行代码居然无法获取请求的完整URL