Ruby net/http 库的一个让人抓狂的 bug

lubyruffy · 2014年09月03日 · 最后由 lubyruffy 回复于 2014年09月28日 · 4762 次阅读

我用 net http 去请求一个 https 的页面,如下两种方法在某些网站返回居然不同: 1、先用 ip 连接,然后手动设置 Host 头 2、直接用 host 连接 绝大部分情况下都是正常的,只有在少部分网站返回的结果完全不一样(一个返回 200,一个返回 404),比如下面的演示:

#!/usr/bin/env ruby require 'net/http' require 'uri' require 'open-uri' require 'openssl'

@uri = URI('https://ebs.shasteel.cn')

def get_ip_of_host(host) require 'socket' ip = Socket.getaddrinfo(host, nil) return nil if !ip || !ip[0] || !ip[0][2] ip[0][2] rescue => e nil end

def test_connect_ip ip = get_ip_of_host(@uri.host) http = Net::HTTP.new(ip, @uri.port) http.use_ssl = true if @uri.scheme == 'https' http.verify_mode = OpenSSL::SSL::VERIFY_NONE http.set_debug_output($stdout) http.start { |h| request = Net::HTTP::Get.new @uri.request_uri request['Host'] = @uri.host response = h.request request puts response.code } end

def test_connect_host http = Net::HTTP.new(@uri.host, @uri.port) http.use_ssl = true if @uri.scheme == 'https' http.verify_mode = OpenSSL::SSL::VERIFY_NONE http.set_debug_output($stdout) http.start { |h| request = Net::HTTP::Get.new @uri.request_uri response = h.request request puts response.code } end

test_connect_ip #will be 400 test_connect_host #will be 200

============我打开了发包调试,可以看到发送和接收的包内容============== opening connection to 61.177.60.85:443... opened starting SSL for 61.177.60.85:443... SSL established <- "GET / HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: /\r\nUser-Agent: Ruby\r\nHost: ebs.shasteel.cn\r\n\r\n" -> "HTTP/1.1 400 Bad Request\r\n" -> "Date: Wed, 03 Sep 2014 11:43:55 GMT\r\n" -> "Server: Apache/2.2.17 (Win32) mod_ssl/2.2.17 OpenSSL/0.9.8o\r\n" -> "Content-Length: 226\r\n" -> "Connection: close\r\n" -> "Content-Type: text/html; charset=iso-8859-1\r\n" -> "\r\n" reading 226 bytes... -> "" -> "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n

\n400 Bad Request\n\n

Bad Request

\n

Your browser sent a request that this server could not understand.
\n

\n\n" read 226 bytes Conn close 400

opening connection to ebs.shasteel.cn:443... opened starting SSL for ebs.shasteel.cn:443... SSL established <- "GET / HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: /\r\nUser-Agent: Ruby\r\nHost: ebs.shasteel.cn\r\n\r\n" -> "HTTP/1.1 200 OK\r\n" -> "Date: Wed, 03 Sep 2014 11:43:55 GMT\r\n" -> "Server: Apache/2.2.17 (Win32) mod_ssl/2.2.17 OpenSSL/0.9.8o\r\n" -> "Accept-Ranges: bytes\r\n" -> "Content-Length: 341\r\n" -> "Last-Modified: Fri, 16 Dec 2011 01:35:36 GMT\r\n" -> "X-Powered-By: Servlet/2.4 JSP/2.0\r\n" -> "Content-Type: text/html\r\n" -> "\r\n" reading 341 bytes... -> "" -> "<%@ page language=\"java\" contentType=\"text/html; charset=UTF-8\"\n pageEncoding=\"UTF-8\"%>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtdn\">\\n... \n\t\t\n\t\t\n\n

\n\n

这就 bug 了?难道两次相同的请求返回必须一样?

#1 楼 @hooooopo 你用其他任何语言的任何库都不会存在这种问题,返回的 http status code 肯定是一致的。

发送的请求都是一样的,那就应该不是 net/http 的问题了吧

#3 楼 @steven_yue 就是因为它显示的请求都一样,但是返回差太远,用其他语言测试都是正常的,所以才说是 http 的 bug。我给的代码是完全可以重现的。

5 楼 已删除

2.host 可以由程序自定义,某些程序为了防止运营商或防火墙拦截会定义虚假 host

#6 楼 @hging 这个没法识别的,如果只看 http request header 部分的话是完全一致的。按这么理解的话,那在 https 的情况下就不能用 ip 连接再设置 host 了。

#7 楼 @lubyruffy 我的意思是某些程序。因为你也说了绝大部分都正常,少部分不正常。所以怀疑是对方网站做了限制呢。

#8 楼 @hging 可以肯定的是对方服务器 https 库实现的问题。可能这属于 net/http 库没有完成某些特性,不一定是 bug。我目前临时解决办法是:先用 ip 连接设置 host,判断如果 https 是返回 400 的话,再用 host 去连接。其他人如果遇到此类问题,可以参考一下。

#9 楼 @lubyruffy 不过比较好奇为什么一定要通过 IP 去连。个人觉得你都已经知道域名了,可以直接用了嘛-.- 还要在通过域名获取 IP 然后再用 IP 去连,再设置 host 好像绕了一圈的样子。

#10 楼 @hging 举个例子,某个网站用了 CDN,那么域名解析的时候会根据来源的省份给你不同的 ip,我想要在一个机器上测试所有的 CDN 节点的响应,就必须每个节点 ip 连接上去,然后手动设置 Host

#11 楼 @lubyruffy 明白了。呜哈哈 涨知识咯。

我猜是 SNI 的原因?楼主这个错误能在 HTTP 下重现嘛

#13 楼 @iBachue 不能,只在 https 下能够准确重现

那就真有可能是 SNI 的原因了 楼主试试换个 SSL 协议?不行的话改 SNI 吧

这个是 SNI 的原因,net/http.rb 的代码里面有这样一段,会将你传入 url 里面的 ip 设置为 hostname:

# Server Name Indication (SNI) RFC 3546
s.hostname = @address if s.respond_to? :hostname=

然后你后续请求的 host 又是通过 header 改变的,一些 web 服务器可能会认为是非法请求,返回了 400 的状态码。 SNI 是在 ruby 2.0 以后加入的,你可以将这行代码去掉试试看,来验证一下是不是 SNI 的问题。

另外如果是你提到监控 CDN 节点的需求,我以前的写过代码是扩展一个 resolv, 能够自定义域名的解析,这样会灵活很多,也是参考了 ruby 本身的代码 lib/resolv-replace.rb,这样就不需要强制设置 Host 和 IP,代码也会简洁很多。

应该就是 SNI 问题,你通过 ip 建立连接,却将 Host header 改为 hostname, 默认 ssl server 会验证他们是否一致的。 不过服务器端可以更改这个设置。所以你看到有的返回 400, 有的 200.

需要 登录 后方可回复, 如果你还没有账号请 注册新账号