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

lubyruffy · September 03, 2014 · Last by lubyruffy replied at September 28, 2014 · 4762 hits

我用 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 Floor has deleted

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.

You need to Sign in before reply, if you don't have an account, please Sign up first.