HTTP代理实现请求报文的拦截与篡改5--将请求报文并转发至服务器
将请求报文并转发至服务器
好的到此原始请求已经获得并且封装了。那么下一步要干什么呢,自然是重新将这个原始请求报文重新包装并发送到目标服务器了。
- HTTP代理实现请求报文的拦截与篡改1--开篇
- HTTP代理实现请求报文的拦截与篡改2--功能介绍+源码下载
- HTTP代理实现请求报文的拦截与篡改3--代码分析开始
- HTTP代理实现请求报文的拦截与篡改4--从客户端读取请求报文并封装
- HTTP代理实现请求报文的拦截与篡改5--将请求报文转发至目标服务器
- HTTP代理实现请求报文的拦截与篡改6--从目标服务器接收响应报文并封装
- HTTP代理实现请求报文的拦截与篡改7--将接收到的响应报文返回给客户端
- HTTP代理实现请求报文的拦截与篡改8--自动设置及取消代理+源码下载
- HTTP代理实现请求报文的拦截与篡改8补--自动设置及取消ADSL拔号连接代理+源码下载
- HTTP代理实现请求报文的拦截与篡改9--实现篡改功能后的演示+源码下载
- HTTP代理实现请求报文的拦截与篡改10--大结局 篡改部分的代码分析
this.Response.ResendRequest()
还记得这句吗,在Session类的Execute方法体内被调用的,这个方法就是干这个事的,下面再简单的分析一下这个方法
我们看到ResendRequest是this.Response的方法而Response是个ServerChatter类型。那么下面,我们当然去到 ServerChatter类里里去看ResendRequest方法了。
一样的只留主干
1 internal bool ResendRequest() 2 { 3 if (!this.ConnectToHost()) 4 { 5 … 6 return false; 7 } 8 try 9 { 10 … 11 if (!this._bWasForwarded && !this.m_session.IsHTTPS) 12 { 13 this.m_session.Request.Headers.RenameHeaderItems("Proxy-Connection", "Connection"); 14 } 15 this.ServerPipe.Send( 16 this.m_session.Request.Headers.ToByteArray(true, true, this._bWasForwarded && !this.m_session.IsHTTPS) 17 ); 18 this.ServerPipe.Send(this.m_session.RequestBodyBytes); 19 } 20 catch (Exception exception) 21 { 22 … 23 return false; 24 } 25 return true; 26 }
上面的一段代码再主干一下,就是下面三句
1 this.ConnectToHost() ; 2 this.ServerPipe.Send( 3 this.m_session.Request.Headers.ToByteArray( 4 true, true, this._bWasForwarded && !this.m_session.IsHTTPS 5 ) 6 ); 7 this.ServerPipe.Send(this.m_session.RequestBodyBytes);
第一句, 后面会分析,就是构造一个ServerPipe,并在里面封装一个和服务端通讯的Socket。
第二句就是通过这个ServerPipe的Send方法将使用this.m_session.Request.Headers.ToByteArray方法重新包装的请求报头发送给服务器 。这个this.m_session.Request.Headers其实就是刚才分析的那个Request。因为this.m_session就是刚才分析的那个Session也就是构造ServerChatter的那个Session,这里简单提一下。
第三句是发送原始请求报文的报体到服务器 。
除了这三个主干,上面的代码里还有一句
this.m_session.Request.Headers.RenameHeaderItems("Proxy-Connection", "Connection");
这个后面会分析到,这里先不要着急。我们先来看this.ConnectToHost() 既然是this这个方法自然就在ServerChatter里了。
详细代码就不帖了,各位自己可打开源代码来看,就在ServerChatter.cs里。
这个方法里主要就是下面三句
this.ServerPipe = new ServerPipe("ServerPipe#" + this.m_session.id.ToString()); Socket socket = CreateConnectedSocket(addressArray, port, this.m_session); this.ServerPipe.WrapSocketInPipe(socket);
第一句,new一个ServerPipe,
第二句,利用CreateConnectedSocket创建一个Socket
第三句,将Socket用ServerPipe包装一下。
第一句没什么讲的,第三句,就是简单的将第二句创建的Socket赋值给ServerPipe内部的baseSokcet而已。主要是第二句,这句是和服务器建立通讯的关键。
当然要想和服务器建立连接,有两样东西是必不可少的,第一是要连接的服务器的IP,第二要连接的服务器的端口,这两个正好是CreateConnectedSocket方法的前两个参数,不知道这两个,说什么都是白搭,这里当然也不例外,所以在调用CreateConnectedSocket方法前,我们必须获得这两个信息,那么这两个信息从那里获取呢,自然是从请求报头里获取,还记得前面讲的请求报头吗,还记得那个Host:www.domain.com首部吗 ? 获取IP和端口就是通过这个HOST首部来完成的。那么如何通过他获得IP和端口呢?
先讲端口,host:www.domain.com。冒号后面的www.domain.com就是要访问的主机的域名,他还有一个更完整的写法 www.domain.com:80,这个冒号后面的就是端口了,但是如果端口是80的话,冒号以及后面的80是可以省略掉的。但如果www.domain.com的WEB服务器监听的是8080端口,那么要访问它的话,就必须写成www.domain.com:8080了,这时候冒号以及后面的端口号是不能省略的。知道了这些,获取端口号是不是变得很容易了,读取冒号后面的那个值不就行了,如果没有冒号以及后面的数字,那么端口自然就是默认的80了
端口搞定了,那么下一步自然就是IP了。IP仍然是通过host这个首部来获得的,host首部冒号后面的部分除了采用 域名:端口 的方法,还可以采用 IP:端口的方法。例如 host:192.168.1.12:8080 。 这种情况下,第二个冒号前面的自然就是IP了,在这里不讨论这种情况,讨论的是使用域名的情况,也就是前面举的那个例子 host:www.domain.com 的情况。这种情况,就需要从域名得到IP了。那么从域名--IP,需要什么技术呢,自然就是DNS(Domain Name System)了。.NET里有对DNS相关操作的封装类System.Net.Dns 。他有个静态方法 GetHostAddresses 就是通过域名得到IP的
IPAddress[] arrResult = Dns.GetHostAddresses(“www.domain.com”);
这个方法的原理其实很简单,就是在本地的DNS缓存里找域名对应的IP,然后封装成IPAddress类型的数组进行返回。为什么返回的是数组? 因为有IPV4和IPV6两种类型的地址,但在一些不支持IPV6的机器上,IPV6项会被筛选掉,所以这种情况下,这个数组的大小就是1。也就是只有一项 。
IP和端口全部有了。那么下一步呢,自然建立和服务器通讯的Socket了。
这时候CreateConnectedSocket就要上场了,我们进去看一看,其实他里面的代码也很简单,主要就是下面二句
socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; socket.Connect(address, port);
这些代码没什么好讲的,就是new一个Socket(System.Net.Sockets命名空间里)对象,然后调用它的Connection方法连接到服务器上去,Connect的第一个参数address就是一个IPAddressr的实例。我们先抛开源码里的实际代码,那么连接www.domain.com服务器的完整的代码应该如下所示:
1 IPAddress[] arrResult = Dns.GetHostAddresses(“www.domain.com”); 2 foreach (IPAddress address in arrResult ) 3 { 4 try 5 { 6 socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp) 7 { 8 NoDelay = true 9 }; 10 socket.Connect(ipv4Address, 80); 11 // IPV4或者IPV6只要有一个连接成功,就可以退出循环了 12 break; 13 } 14 catch (Exception exception2) 15 { 16 exception = exception2; 17 } 18 }
到此,我们就已经连接上服务器了,后面就可以通过这个Socket往服务器转发请求,并且也可以通过它,读取服务器的响应报文了。当然再回忆一下刚才讲的,这个Socket实际上是通过 this.ServerPipe.WrapSocketInPipe(socket); 被封装在了ServierChatter类的ServerPipe属性里,所以后面一般是通过ServerPipe来进行和服务端的 通讯 。
OKAY。 ClientChatter类的ConnectToHost()已经分析完了,那么我们再例行性的回忆一下刚才所讲的,并思考一下,这个方法执行完成后,我们的程序里发生了那些变化。其实很简单,就是ClientChatter类里的 ServerPipe 对象被实例化了,并且封装了一个连接到目标服务器的Socket。那么我们后面就可以在ClientChatter类里通过 this.ServerPipe.Send方法发送请求到目标服务器了。是不是又有一种熟悉的感觉了。把前面帖过的代码,再帖过来一遍。
ClientChatter类的 ResendRequest方法的主干部分
this.ConnectToHost() this.ServerPipe.Send( this.m_session.Request.Headers.ToByteArray( true, true, this._bWasForwarded && !this.m_session.IsHTTPS ) ); this.ServerPipe.Send(this.m_session.RequestBodyBytes);
还记得这段代码吗?
看到this.ConnectionToHost()方法后面的代码了吧。现在应该基本上能明白怎么回事了吧
有一个地方要注意一下,this.m_session.Request.Headers.ToByteArray 方法的作用,就是把刚才封装的报头信息(HTTPRequestHeaders类型的实例:Headers),再重新转变成二进制流的形式(虽然HTTP协议是采用的是字符方式,但传输是基于TCP的,Socket就是对TCP的封装,而TCP传输的是二进制流,所以使用Sokcet发送数据时,一定要转成二进流的方式,读取时读取的也是二进制流)。有兴趣的可以自己去读读这个方法的源码,这个方法,既然是Headers的方法(Headers.ToByteArray),而Headers又是HTTPRequestHeaders类型,这个方法,自然是在HTTPRequestHeaders这个类里了。具体的就不详细的一句一句的分析了,反正做的工作就如刚才所说是把分析出来的请求报文做些适当的调整后,再重新转变成二进制流(byte[]),才刚提到过HTTP协议的报文都是字符型的,所以转变成二进流的方式,就是 Encoding.ASCII.GetBytes(“报文内容”);
例如
byte[] bytesHeaders = Encoding.ASCII.GetBytes( @"post /index.html http/1.1 host:www.domain connection:close ");
当然你也可以一行行的转,或者一部分一部分的转,在ToByteArray里就是一部分一部分转。每转一部分就追加到stream(MemoryStream 类型)变量里,最后stream.ToArray();
还有一个地方要说明一下的。
1 if ( 2 includeProtocolInPath 3 && !this.HTTPMethod.Equals("CONNECT", StringComparison.OrdinalIgnoreCase) 4 ) 5 { 6 byte[] buffer4 = 7 base._HeaderEncoding.GetBytes( 8 this._UriScheme + "://" + this._uriUserInfo + base["Host"] 9 ); 10 stream.Write(buffer4, 0, buffer4.Length); 11 }
if条件里的includeProtocolInPath 条件的值,其实就是ToByteArray的最后一个参数,从前面的代码看就是 this._bWasForwarded && !this.m_session.IsHTTPS 从英文的字面意思看就是 是否继续向前,或者不是HTTPS协议。 也就是如果是继续向前而且不是HTTPS协议的话就为真,那么再联系后面那个CONNECT一起判断,就是如果是继续向前,并且不是HTTPS协议,而且不是CONNECTION方法的话,就执行大括号里的语句体。那么大括号里的语句体是干什么的,代码就不分析了,在这里我举个简单的例子,你们就明白了。
还记得前面的请求报文格式吗
post / http/1.1 <method><request-url><version> CRLF host:www.domain.com <header> CRLF content-length:8 CRLF CRLF a=b&b=cd <entity-body>
如果执行大括号里的过程体,这段报文会变成什么样呢。
post http://www.domain.com/ http/1.1 <method><request-url><version> CRLF host:www.domain.com <header> CRLF content-length:8 CRLF CRLF a=b&b=cd <entity-body>
看到区别了吗。为什么要做这样的处理呢。这要从浏览器直接向目标服务器和向代理服务器发送的请求报文的区别谈起。
当没有设置代理的时候,正常的HTTP请求报文格式应该是这样的
post / http/1.1 <method><request-url><version> CRLF host:www.domain.com <header> CRLF content-length:8 CRLF connection:keep-alive CRLF CRLF a=b&b=cd <entity-body>
但是如果设置了代理,那么浏览器的请求报文都会被发送到代理服务器,在这种情况下,浏览器会对请求报文做些简单的变化,主要在两个方面。
一是
<request-url>部分,我们看到在直接发送到目标服务器的情况下,一般都是相对的地址(前面的例子默认都是这种情况下的)
post / http/1.1
而在发送到代理服务器的情况下,就会变成
post http://www.domain.com/ http/1.1
<request-url> 部分被替换成了完整的URL地址。为什么要这样做呢,是因为早期的HTTP协议,是没有HOST首部的,这样当直接连接到服务器时,是没有问题的,因为服务器IP在发送报文前肯定是已知的,要不然我要把报文发给哪个呢? :) 所以这种情况只要传个相对路径给服务器,服务器就可以通过这个相对路径找到资源并返回了。但是如果是发送到代理的话,这个请求报文就出问题了,因为代理无法从这个请求报文里分析出来,目标服务器的地址,地址都不知道,代理又如何将这个请求转发呢。所以,在HTTP协议里将发送到代理服务器情况下的请求报文里的<request-url>设计成了完整地址,这样代理服务器就可以通过分析这个完整的址址的主机部分获取目标服务器的地址,如此就可能顺利的建立到目标服务器的连接并实现转发了。但是后来随着虚拟主机的出现,同一个服务器可以映射多个站点,没有HOST首部的HTTP协议,已经没办法处理这种情况了,所以后来HTTP协议里引入了HOST这个首部,理论上在引入了HOST首部后,不仅虚拟主机的情况可以解决,就连代理服务器也可以一并解决了,就不用再在<request-url>里写完整地址了,但是为了保持协议的兼容性,当发送到代理服务器时<request-url>为完整地址的规则还是被继承了下来。
那么第二个区别是什么呢。那就是当直接发送到目标服务器时,有个首部是
connection:keep-alive/close
当发送到代理服务器时,这个首部会被替换成
proxy-conection:keep-alive/close
可能细心的读者已经联想到了前面没有细说明的一段代码了,我们现在再把他拷贝到这里来。他就是
if (!this._bWasForwarded && !this.m_session.IsHTTPS) { this.m_session.Request.Headers.RenameHeaderItems("Proxy-Connection", "Connection"); }
这段代码在 ServerChatter 类里的 ResendRequest方法体里。
这里又出现了this._bWasForwarded. 这个变量我们在前面按字面意思把他翻译成了,是否继续向前,现在我们先继续使用这个翻译,然后把这个翻译也放到这段代码里,那么这段代码的意思就是,如果不是继续向前,而且不是HTTPS的情况下,就把proxy-connection 替换成 connection .
那么这个“继续向前“究竟是什么意思呢,也就是在什么情况下,这个变量才会是TRUE呢,下面就简单说明一下,要不然仅靠这个字面的翻译前面的代码恐怕还是没办法理解的。
这个变量,只在一种情况下,才会是TRUE,就是明确的知道,我要转发请求报文的目标服务器仍然是个代理服务器或者是个网关的情况下,才为TRUE。然而程序本身是没有办法判断出要转发到的目标服务器究竟是不是代理服务器或者网关的,所以这个变量,不在人工干预的情况下,百分之百是FALSE的 。 在我们的代码里,目前并没有实现人工干预的功能,所以这个变量永远都是FALSE。保留他下来只是为了以后的扩展。因为事实上,代理服务器确实不一定会把请求直接转发到最终的目标服务器,而是有可能先转发到另外一个代理服务器或者网关上,然后再由他们转发到最终的目标服务器或者他也是再转发至下一个代理服务器或者网关(每转发一次用专业术语讲就是下一跳,对没错,是下一跳,不是吓一跳 :) )。我日,搞这么复杂有意思吗?NO,NO,当然有意思了 ,因为我们很有可能会遇到下面的这种情况, 我们在自己的内网建了一个过滤代理,这个过滤代理的功能就是把除了公司想让我们访问的网址以外的所有网址全部屏蔽,但是现在出问题了,公司想让我们访问的网站,全部都是被天朝封锁的国外网站,这可怎么办呢? 其实不难办,这时候只要过滤代理再把这个请求转发给另外一个代理服务器(这个服务器要求在国内能访问,另外它也能访问国外的网站就可以了),然后由它来获取网页后响应给我们的过滤代理,再由我们的过滤代理发回给客户端 。