构建HTTP代理服务器(210分)

  • 主题发起人 主题发起人 jieking1128
  • 开始时间 开始时间
J

jieking1128

Unregistered / Unconfirmed
GUEST, unregistred user!
构建自己的HTTP代理服务器
 
在网上看到的一篇文章。
很多朋友一定象我一样需要一个可以自己定制的HTTP代理程序,受到论坛里面的LXM365朋友的启发(在此感谢一下!),发现利用DELPHI7 的INDY控件IdMappedPortTcp可以做成HTTP代理程序.于是我仔细查看了一下IdMappedPortTcp的源代码,发现Lxm365在OnExecute事件里面处理的方案并不完美,因为在触发OnExecute事件前代理程序必须已经连接到一个目标机器,所以他的代码中有:
TidTcpClient(AThread.OutboundClient).Host:=RequestHost;
TidTcpClient(AThread.OutboundClient).Port:=RequestPort;
TidTcpClient(AThread.OutboundClient).Disconnect;
TidTcpClient(AThread.OutboundClient).Connect(AThread.ConnectTimeOut);
这就表示需要断开已有的连接,重新连接到另外一个目标.这么表叙吧.有A,B,C,D4个机器,A是客户机,B是代理服务器,C是B机上设置的默认访问的WEB服务器,D是需要访问的WEB服务器,当A连接到B时,B将连接到默认的C主机,然后B机根据A的请求,修改MAPPEDHOST和MAPPEDPORT为D机,然后断开B机与C机的连接,接着马上连接到D机.这样的话,C机必须存在,不然的话应该无法触发ONEXECUTE事件,那么也就无法继续代理服务了.那么我们怎么解决这个问题呢?我的想法是在A机连接到B机时暂时不影射到默认的C机上,而是等待A机的请求数据,根据HTTP请求提供的信息来修改MAPPEDHOST和MAPPEDPORT,然后由控件自动实现连接到需要的D机,这样A机到B机的连接就被影射到了D机上了,相当于A机直接访问了D机的WEB端口,那么我们就可以做一个函数ReceiveData()来获取来自A机的请求包,这时有个问题要注意,我们必须把从A机收到的请求内容转发给D机呀,什么时候发呢?当然是一连接到D机的时候就发呀,所以我们需要另外一个函数SendData()来发送请求,这可不能忘,嘿嘿,直接转发可不行咯,HTTP协议里面客户端访问WEB服务器时,通过代理和不通过代理的请求包内容可是不同的.所以我们必须要处理.那么我们就需要另外一个函数ProcessData()来分析和修改处理请求包.因此整个HTTP代理服务器的实现变成了这样:
1.A机向代理机B产生连接并发送请求包
2.B机接收A机连接并读取A机的请求(此时B机已经有专门的线程来控制该连接)
3.B机在影射到目标机器前分析请求包,并根据请求修改该连接线程的MappedHost和MappedPort为D机的地址和端口
4.B机的连接线程连接到预定的MappedHost,并将已经修改过的请求包发送给D机.
5.D机返回的数据包直接由连接线程控制返回给A机,不用我们干预,完全自动.
6.由于我没有更多资料,我所了解的HTTP协议就是"连接-->请求<--->应答-->断开"模式,因此我认为客户端基本上只向WEB服务器发送一次请求,比如GET,HEAD,POST方法,其他的我不太清楚,如果在没有断开时又有请求包,那么就需要在OnExecute里面处理请求包了.谁有详细的HTTP协议中文资料呀,特别是HTTP代理部分.请一定给我一份,我的EMAIL:qianlj@gtjas.com

好了,说了一大堆想法,让我们实践一下了.我们新建一个程序,在FORM上放上以下控件:
1.Checkbox1:TCheckBox (用于控制MappedPortTcp控件的ACTIVE属性的TURE和FALSE)
2.LocalPort:TLabeledEdit(D7中才有,本地代理服务端口,一般设为80或8080)
3.DefaultHost:TLabeledEdit(默认影射到的主机地址,域名或IP都可以)
4.DefaultPort:TLabeledEdit(默认影射到的端口)
5.Log:TCheckBox (控制是否显示传输的内容,可以监控你的用户访问了些什么内容哟)
6.Memo:TMemo (用于记录传输的内容)
7.MappedPortTcp:TIdMappedPortTcp (这可是核心的东西呀,全都靠他了!!!)
8.IdAntiFreeze1:TIdAntiFreeze1 (这个可要可不要,好象INDY中介绍,用了他可避免界面不响应的问题)
继续....写代码,在MappedPortTcp的事件中添加以下函数吧.
1.在OnConnect中
ReceiveData(Athread);
if log.Checked then begin
memo.Lines.BeginUpdate;
memo.Lines.Add('--------连接请求数据包-------------');
memo.Lines.Add(athread.NetData);
memo.Lines.EndUpdate;
end;
if Athread.Connection.Connected then
ProcessData(Athread);
if log.Checked then begin
memo.Lines.BeginUpdate;
memo.Lines.Add('------修改过的连接请求数据包------');
memo.Lines.Add(athread.NetData);
memo.Lines.EndUpdate;
end;
2.在OnOutBoundConnect中:
if not Assigned(AException) then
if not SendData(Athread) then begin
memo.Lines.Add('----------------发送数据出错--------------------')
end;
3.在OnOutBoundData中
if log.Checked then begin
memo.Lines.Add('------服务端返回数据包----------');
memo.Lines.Add(athread.NetData);
end;
4.在OnExecute中
//目前我没管他,如果一次连接中,HTTP的请求不是一次而是多次,那么在这个事件中也要对来自A机的后续请求包进行处理,用ProcessData(Athread)就应该可以了.
OK,完成了,不会吧就这么简单,对就这么简单,嘿嘿,别急,还没搞定呢,我们还需要看看关键的三个函数:ReceiveData(Athread),ProcessData(Athread),SendData(Athread).

1.获取来自A机的请求数据.将数据直接保存到Athread.NetData.
function TMain_Form.ReceiveData(Athread: TIdMappedPortThread):boolean;
begin
with Athread do begin
NetData:='';
result:=false;
try
Connection.ReadFromStack(true,-1,true);
NetData:=Connection.InputBuffer.Extract(Connection.InputBuffer.Size);
result:=true;
except
end;
end;
end;
2.当连接到目标机D时,将请求包Athread.NetData发送给目标机器D.
function TMain_Form.SendData(Athread:TIdMappedPortThread):boolean;
begin
result:=false;
if Assigned(Athread.OutboundClient) and Athread.OutboundClient.Connected then begin
try
if Athread.Connection.Tag=1 then begin
Athread.NetData:='';
//对HTTP隧道技术的支持,如果连接目标机器成功则向客户端返回连接成功信息
Athread.Connection.WriteLn('HTTP/1.0 200 OK'+EOL);
end;
if length(Athread.NetData)<>0 then
Athread.OutboundClient.Write(Athread.NetData);
result:=true;
except
memo.Lines.Add('------转发数据出错!!!------')
end;
end;
end;
3.处理来自A机的请求,并且根据请求改变将要影射到的目标机器地址和端口
function TMain_Form.ProcessData(AThread: TIdMappedPortThread):boolean;
var
RemoteHost:TIdURI;
InputLine,HttpCmd,HttpVer:string;
HttpHeader:TIdHeaderList;
TempHeader:TStringList;
Proxy:boolean;
begin
HttpHeader:=TIdHeaderList.Create;
TempHeader:=TStringList.Create;
result:=true;
try
HttpHeader.UnfoldLines:=false;
HttpHeader.FoldLines:=false;
HttpHeader.Text:=Athread.NetData;//将请求包放取出来处理
RemoteHost:=TIdURI.Create('HTTP://'+HttpHeader.Values['Host']);

TempHeader.CommaText:=HttpHeader.Strings[0];//获取HTTP请求行
Proxy:=length(HttpHeader.Values['Proxy-Connection'])>0;
//是否代理请求包,我是通过Proxy-Connection标志判断的,但是我跟踪发现有些程序通过HTTP代理连接时没有这个标志,我不知道是他们的错,还是我对HTTP协议了解不够,但是IE是有这个标志段的.
//请求行至少有三段,由空格分开:命令 URL HTTP版本,所以Count需要>2
if TempHeader.Count>2 then begin
HttpCmd:=UpperCase(TempHeader.Strings[0]);//HTTP命令
HttpVer:=TempHeader.Strings[TempHeader.count-1];//HTTP协议版本
InputLine:=TempHeader.Strings[1];//请求的URI
//如果是代理请求包
if Proxy then begin
//分别处理不同的请求命令,并重建HTTP请求行,因为直接访问WEB服务器的请求包中的第一行中是不包含域名和端口信息的,而通过代理时有完整的URL,我们删除了域名信息就相当与在代理机上模拟了一个普通客户端请求.
TempURI:=TIdURI.Create(InputLine);
if (HttpCmd='HEAD') or (HttpCmd='GET') or (HttpCmd='POST') then begin

HttpHeader.Strings[0]:=HttpCmd+' '+TempUri.Path+TempUri.Document+TempUri.Params;
if TempUri.Bookmark<>'' then
HttpHeader.Strings[0]:=HttpHeader.Strings[0]+'#'+TempUri.Bookmark;
HttpHeader.Strings[0]:=HttpHeader.Strings[0]+' '+HttpVer;
end;

if HttpCmd='OPTIONS' then begin
end;
if HttPCmd='TRACE' then begin
end;
if HttpCmd='PUT' then begin
end;
if HttpCmd='DELETE' then begin
end;
//删除请求包中的代理标志
HttpHeader.Delete(HttpHeader.IndexOfName('Proxy-Connection'));
if HttpCmd='CONNECT'then begin
RemoteHost.URI:='HTTP://'+InputLine;
Athread.Connection.Tag:=1;
//这里是对HTTP隧道技术的支持,代理程序连接到目标机器后需要向客户机返回一个连接成功信息,我偷懒就直接标记了一下用于判断.在OnOutBoundConnect中会用到
end;
TempURI.free;
end;

if Assigned(AThread.OutboundClient) and Athread.Connection.Connected then begin
if not Athread.OutboundClient.Connected then begin
TidTcpClient(AThread.OutboundClient).Host:=trim(RemoteHost.Host);
TidTcpClient(AThread.OutboundClient).Port:=StrToIntDef(RemoteHost.Port,TidTcpClient(AThread.OutboundClient).Port);
end;
end;
end;

Athread.NetData:=HttpHeader.Text;
//将修改过的数据保存到Athread.NetData中,这里有一个小问题,一般情况下如果请求包中不包含特别数据,最后两个字节是#D#A,而如果包含数据则不需要添加#D#A,但是上面这句产生的ATHREAD.NETDATA的最后两个字节总是#D#A,本来是要判断一下的,但是我懒,没试,刚才看到这才想起来,应该可以这么判断一下,大家可以试试,不过我之前没判断好象也可以正常使用,嘿嘿
//if StrToIntDef(Trim(HttpHeader.Values['Content-Length']), -1)>0 then begin
// SetLength(Athread.NetData,Length(Athread.NetData)-2);
// 删除末尾的#D#A(换行回车符)
//end
RemoteHost.Free;
finally
TempHeader.Free;
HttpHeader.Free;
end;
end;


OK,OK,这下应该了解了吧,是不是很简单?就想使用?哈哈,当心......还没完呢......别用手指着我,我可是"横眉冷对千夫指,俯首甘为孺子牛"呀,呵呵.我老实交代了:
1.在程序的uses中有这些东西吗?
IdTcpclient,IdMappedPortTCP,IdURI,IdHeaderList,IdGlobal;没有?那得加上.
2.把以下内容放到 private与public之间
function ReceiveData(Athread: TIdMappedPortThread):boolean;
function ProcessData(AThread: TIdMappedPortThread):boolean;
function SendData(Athread:TIdMappedPortThread):boolean;
. 3.在Checkbox1的OnClick中添加
begin
try
if not MappedPortTcp.Active then begin
MappedPortTcp.DefaultPort:=strtointdef(LocalPort.Text,80);
MappedPortTcp.MappedHost:=DefaultHost.Text;
MappedPortTcp.MappedPort:=strtointdef(DefaultPort.Text,80);
end;
MappedPortTcp.Active:=checkbox1.Checked;
except
end;
checkbox1.Checked:=MappedPortTcp.Active;
end;

哎呀,好累呀,手都酸了,不多写了,还有好多东西自己都没完全了解呢,我现在计划在这个里面多加点内容,比如限制可访问代理的IP地址,每连接的流量控制,还得加上代理的用户验证(在HTTP请求包分析函数里面处理),还可以添加域名的转向和站点的访问限制,还有不同类型文件的下载控制等等,......,不知道大家还需要什么功能,我试试看能否实现.大家都给点建议,呵呵,当然也需要大家多给评点分.
最后想到一点.这个程序有个功能不知道大家发现了没有?.........
当程序运行在一台跨公网和内网的机器上时,内部网的用户将IE中代理的IP地址设为该机的内网地址就可以很好的访问INTERNET了,废话...这是什么新功能!!!,别急,如果你的客户端是公网IP,他的IE中代理的IP地址设为该机器的公网IP的话,在地址栏中输入http://你的内部WEB服务器IP 的话,不是就可以访问你的内部网了吗.呵.有用吧,可怕吧.怎么控制呢?简单,在服务ACTIVE:=TRUE前只绑定内部IP就可以了(也就是只在内部网内提供服务),怎么绑定?累了,不说了,看DELPHI的DEMOS吧.俺就不罗嗦了.
最后希望大家多多鼓励我,如果这篇文章你喜欢,请在转贴时注明作者和出处.这可是我的处女作哟.嘿嘿,呵呵,哈哈.晚安!!!
 
接受答案了.
 
后退
顶部