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

来自公#众#号:Gopher指北

缘起

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

image

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

image

从图中可以知道,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.0
var s string
if s, err = tp.ReadLine(); err != nil {
    return nil, err
}
  1. 将请求行的内容分别解析为req.Methodreq.RequestURIreq.Proto
var ok bool
req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
  1. req.RequestURI解析为req.URL
rawurl := req.RequestURI
if 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在本文均被译作请求资源)。

image

关于请求方法有哪些想必不用老许在这儿科普了吧。至于常用的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.1
Host: 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的定义。

image

通过上面这张图大概能够猜出来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信息。

image

从图中可以发现,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

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容