zoukankan      html  css  js  c++  java
  • HTTP代理实现请求报文的拦截与篡改3代码分析开始

    返回目录 

      下面我们就来分析一下源代码,详细的代码可以看源码,在这里为了使实现的思路更加的清晰,我们只对部分关键的代码进行说明         

      Fiddler 的设计思想还是相当不错的,在程序启动的时候,new 一个代理(Proxy)类的实例,然后调用这个实例的Start方法,来启动代理服务,在Start方法里就是不停的异步监听本机的8888端口(还记得刚才设置代理服务器时设置的端口吗),如果监听到了,就从线程池里,取出来一个线程,并在这个线程里,构造一个Session对象。一个Session对象,代表客户端与服务器的一次会话,从开篇的两张图可以知道,在有代理服务器情况下的一次会话(Session)代表的是1.从客户端读请求,2.重新包装客户端的请求,转发至目标服务器. 3.从目标服务器读取响应信息 4.包装接收到的响应信息并返回给客户端。故而在Session类里,封装一个ClientChatter类型的名为Request的对象,用来实现和客户端的通讯,另外又封装了一个ServiceChatter类型的名为Response的对象,用来实现和目标服务器的通讯。 ClientChatter和ServiceChatter是对通讯的高层封装,而原始的数据流,则分别由ClientChatter里的ClientPipe(客户端管道)和ServiceChatter里的ServicePipe(服务端管道)来进行读取。ClientChatter 调用 ClientPipe的Receive方法来读取原始请求流,并对原始的请求流进行分析,将分析出来的HTTP头信息保存在 Headers变量里,并将其它相关信息,存放在相应的变量里。ServiceChatter的功能类似,只是通讯的对象由客户端变成目标服务器而已 。

    我们的代码基本上沿袭Fiddler的 思想  。          

    下面来看具体的代码。             

    先从Program.cs开始        

    1 [STAThread]
    2 static void Main()
    3 {  
    4     Application.EnableVisualStyles();
    5     Application.Run(new FrmMain());  
    6 }

    如果不刻意的在工程属性里设置,这里就是整个程序的入口点。 

    从上面的代码可以看出来,默认启动了 FrmMain 窗体类      

    FrmMain.cs 

    顺藤摸瓜

    下一步我们来看FrmMain窗体类的构造方法,界面部分略去,直接看最后两句。

    1 proxy = new Proxy();
    2 proxy.Start(8888);  

    在这里,我们的核心类 Proxy 出现了,刚才已经讲过,当调用Start方法时,我们的代理服务就正式启动了。      

    Start方法,只有一个参数listenPort

    internal bool Start(int listenPort)

    就是我们的代理服务器要监听的端口,在这里我们写死成了8888 。 proxy.Start(8888) ;     

     

    Net/Proxy.cs   

    再到Proxy.Start方法体里看看有些什么。   

     1 internal bool Start(int listenPort)
     2 {
     3     try
     4     {
     5         this.acceptor = 
     6             New Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
     7          this.acceptor.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), listenPort));
     8          this.acceptor.Listen(50);
     9          this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null);
    10     }
    11     catch (Exception e) 
    12     {
    13         MessageBox.Show(e.Message);  
    14         return false ; 
    15     }
    16     return true;  
    17 }                  

    这个代码很简单,就是创建一个Socket对象,然后,绑定到本机(127.0.0.1)listenPort(8888)端口  

    this.acceptor.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), listenPort)); 

    然后开始监听这个端口

    this.acceptor.Listen(50); 

      参数50表示等待处理的连接队列的最大值,这个有点难理解,说简单点就是,当客户端请求连接服务端(我们这个就是服务端)时, 服务端会将监听到的请求都放到一个队列里(50就是设置这个队列的最大值的),然后服务端通过Accept方法,从这个队列里取出来一个请求,然后依据这个请求,创建一个和客户端的连接,并用这个连接和客户端进行通讯。 也就是说如果在服务端,一直都不Accept的话,客户端最多只能连接50次, 超过50次,服务器就会直接拒绝请求,因为等待队列已经满了 。 

    在这里引出了一个方法:Accept . 这正是我们下一步要用到的。看下一句 

    this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null);

    这里出现了一个BeginAccept方法  

      Socket里的方法有个规律,类似Begin***的方法,肯定有个End***方法和他对应,这是用于异步处理的。而没有BeginEnd前缀的***方法,则是同步处理用的。因此this.acceptor.Accept 这个方法就是用于同步接收的方法了,当然这里我们是不能使用同步接收的方法的,我们可不想,如果队列里没有连接的时候,程序堵在Accept这里不动。   

    BeginAccept的第一个参数,是异步处理的委托,这里我们设置成了AcceptConnection方法。 也就是一旦我们的服务端监听到了连接,就会马上调用AcceptConnection方法进行处理。        

    下面我们继续进入AcceptConnection方法   

     1 private void AcceptConnection(IAsyncResult ar)
     2 {
     3     try
     4     {
     5         ThreadPool.UnsafeQueueUserWorkItem(new WaitCallback(Session.CreateAndExecute),  this.acceptor.EndAccept(ar));
     6     }
     7     catch (ObjectDisposedException)
     8     {
     9          return;
    10     }
    11     catch (Exception)
    12     {
    13          return;  
    14     }
    15     try
    16     {
    17          this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null);
    18      }
    19      catch (Exception)
    20      {
    21      }  
    22 }       

     

    忽略异常处理的部分(当然这里处理的相关粗糙),这段代码,其实只有两句  

    // 1:  
    ThreadPool.UnsafeQueueUserWorkItem(
        new WaitCallback(Session.CreateAndExecute), 
        this.acceptor.EndAccept(ar)
    );   
            
    // 2:  
    this.acceptor.BeginAccept(new AsyncCallback(this.AcceptConnection), null); 

      第一句是从线程池里取出来一个线程,并在这个线程里执行Session.CreateAndExecute 方法。具体的说明,可以查MSDN。在这里只要明白,它就相当于在另外一个线程里执行这句 

    Session.CreateAndExecute(this.acceptor.EndAccept(ar)); 

    就行了   

    这里this.acceptor.EndAccept(ar) 要注意一下  。 

      上面也提到了。Begin***End***是成对出现的,在Proxy.Start方法里,出现了一个BeginAccept,这里自然要出现一个EndAccept和他对应,同时   

    this.acceptor.EndAccept(ar) 的返回值是一个新的Socket ,这个Socket就是前面提到的用于和客户端进行通讯的Socket了。在这里直接将这个Socket作为参数传递给了 Session.CreateAndExecute  方法  

      第二句和Proxy.Start里一样的,就是再继续监听有没有其它请求,如果监听到了,就再调用Proxy.AcceptConnection处理 。   

    这样我们就可以不间断的处理所有来自客户端的请求了。   

    Net/Session.cs 

    好了 Proxy.AcceptConnection 讲完了,下面自然要进入 Session.CreateAndExecute  方法了。 Session.CreateAndExecute  方法在 Net目录下的Session.cs  里。 

    定义如下 :   

    public static void CreateAndExecute(object param)  
    {
        ClientPipe clientPipe = new ClientPipe((Socket)param);
        Session session = new Session(clientPipe, null);
        session.Execute();
    }  

      这是一个静态方法,主要是用来创建一个Session类的实例。并执行Session实例的Execute方法,这里只有一个参数,这个参数就是从Proxy.AcceptConnection 方法里传进来的用来和客户端通讯的那个Socket  ;       

      前面已经提到过,Session类是用来封装代理服务器的一次会话,而代理服务器的一次会话表示,从客户端读请求,然后转发至服务器,然后再读取服务器的响应,然后再将响应转发回客户端,所以在代理的一次会话中,需要一个和客户端通讯的SOCKET从客户端读请求,并将响应发回给客户端,另外还需要一个和服务端通讯的SOCKET往服务端转发请求,并读取 服务端的响应 。                  

    所以Session类有一个两个参数的构造方法(第二句)

    public Session(ClientPipe clientPipe, ServerPipe serverPipe)

    前面也提到过,ClientPipe 和 ServerPipe 其实就是对于Socket的封装。

    所以ClientPipe有一个Socket类型参数的构造方法 

    ClientPipe clientPipe = new ClientPipe((Socket)param); 

      在这里第二个参数是 null 。 也就是ServerPipenull,后面我们会看到,这个是在转发请求到服务器时创建的(NEW出来的)。为什么要延迟到那里再NEW,其实很容易理解,因为到目前为止,我们还没有开始读取客户端的信息,这样我们就不知道客户端究竟要将请求发送到哪里,自然就没办法建立一个到目标服务端的管道了。Fiddler创建一个这样的构造方法,是因为ServerPipe的重用,但ServerPipe的重用我们给简化掉了,不过这样的构造形式仍然保留了下来,至于理由嘛,就是这样看起来很对称 :)   。                                      

      

    下面我们来看看Session的这个构造方法。 

     1 public Session(ClientPipe clientPipe, ServerPipe serverPipe)
     2 {
     3     this.Timers = new SessionTimers();
     4     this.Timers.ClientConnected = DateTime.Now; 
     5     this.Flags = new StringDictionary();
     6     if (clientPipe != null)
     7     {
     8         this.clientIP 
     9             = (clientPipe.Address == null) ? null : clientPipe.Address.ToString();
    10 
    11          this.clientPort = clientPipe.Port;
    12          this.Flags["x-clientIP"] = this.clientIP;
    13          this.Flags["x-clientport"] = this.clientPort.ToString();
    14          if (clientPipe.LocalProcessID != 0)
    15          {
    16             this._localProcessID = clientPipe.LocalProcessID;
    17             this.Flags["x-ProcessInfo"] 
    18                 = string.Format(
    19                     "{0}:{1}", 
    20                     clientPipe.LocalProcessName, 
    21                     this._localProcessID
    22             );
    23             this._localProcessName = clientPipe.LocalProcessName;  
    24          }
    25      }
    26 
    27      this.Response = new ServerChatter(this);
    28      this.Request = new ClientChatter(this);
    29      this.Request.ClientPipe = clientPipe;
    30      this.Response.ServerPipe = serverPipe;
    31 }

     

    这里开始就实例化了一个SessionTimers

    this.Timers = new SessionTimers();

      这个类在Fiddler里是用来记录一次会话过程中的各个阶段的时间点的,主要是用来分析会话的过程中各个阶段所花费的时间。在这里我们保留下来以备后用,但是对这次例子用处不大。可以忽略掉。 

    下面又实例化了一个StringDictionary类型的Flags对象,

    this.Flags = new StringDictionary();

      用来存储Session过程中的一些标志信息,例如客户端的进程信息,是否设置了断点,断点类型等等。这个标志信息是很重要的,他可以使我们方便的在不同的方法甚至类中传递一些信息.  

    下面一句是判断clientPipe是否为空。如果不为空(这里不为空,我们在Session.CreateAndExecute方法里已经构造了一个实例,并传递过来了)就读取一些信息,写到 this.Flags 里。  

      这里稍稍再详细一点的对ClientPipe做个说明,刚才讲过了,ClientPipe其实就是对负责和客户端通讯的那个Socket的一个封装,所以除了提供基本的通讯功能外,又进一步对一些客户端的信息进行了封装,例如,客户端的IP,端口,进程名和进程ID等等。

      再往下四句 

    this.Response = new ServerChatter(this);
    this.Request = new ClientChatter(this);
    this.Request.ClientPipe = clientPipe;
    this.Response.ServerPipe = serverPipe;

      不知道还记不记得最开始的那段分析,我们提到了。ClientChatter,ServerChatterClientPipe,ServerPipe的区别,ClientChatter,ServerChatter是较高层次的封装,例如ClientChatter类就提供了对于客户端请求头的封装,你要想获取请求的头信息,可以这样获取 ClientChatter的实例.Headers ,Headers是一个HTTPRequestHeaders类型的对象,后面我们会详细的讲讲这个类,这里先点出来一下。当然在封装这些信息之前,需要先从客户端读取原始的HTTP头和内容信息,这个就要通过ClientChatter调用ClientPipe完成了。

      所以总结一下就是, ClientPipe负责从客户端读取原始请求信息,并简单的封装一下客户端的相关信息,而ClientChatter会对这些原始请求进行进一步的封装。 以方便后续的调用。  ServerChatterServerPipe是同样的道理 。    

     

    知道了这些,上面那四句话,自然就容易理解了。 

      创建一个ServerChatter类型的Response对象,用来和服务端进行通讯和获取相关的信息。再创建一个 ClientChatter   类型的Request对象,用来和客户端进行通讯并获取相关的信息,可以看到Request有一个ClientPipe属性,我们将在Session.CreateAndExecute里创建的clientPipe对象赋值给了它,而这个对象就是对和客户端通讯的Socket的一个封装  

     

    好了,看完了Session的这个构造方法,我们重新回到Session.CreateAndExecute方法。现在只剩下最后一句了。  

    session.Execute();

    这个就不用讲了吧,基本上是个人类都能看明白了,就是调用Session类的Execute方法。

    所以下面我们进入Session.Execute方法 。  

    当然在进入Execute方法之前,    

    我们需要先回顾一下刚才所分析出来的东西:  

    session 这个对象里现在已经有了哪些东西

    session:Session
    -- Request:ClientChatter
    -- ClientPipe:ClientPipe  =  new ClientPile(和客户端通讯的Socket)
    -- Response:ServerChatter        
    -- ServerPipe:ServerPipe  =  null 
    -- Flags:StringDictionary 

    好的,有了这些储备后,我们就可以正式进入Session.Execute方法了   。   

    Execute里面代码相对较多,为使思路清晰,我们去掉一些细节部分,只保留主干部分,如下所示:                          

     1 internal void Execute()
     2 {
     3     if (!this.ObtainRequest()){return;}  // 获取请求信息  
     4     if (this.State < SessionStates.ReadingResponse)
     5     {
     6         if (!this.Response.ResendRequest()) // 将包装后的请求重新发到目标服务器
     7         {
     8             this.CloseSessionPipes(true);
     9             this.State = SessionStates.Aborted;
    10             return;
    11         }
    12 
    13         Intercepter.UpdateSession(this);    
    14 
    15         if (!this.Response.ReadResponse ())  // 读取从目标服务器返回的信息 
    16         {
    17             if (this.State != SessionStates.Aborted)
    18             {
    19                 this.Request.FailSession(0x1f8,
    20                   "Receive Failure", "ReadResponse() failed: The server did not return a response for this request."
    21                 );
    22              }
    23              this.CloseSessionPipes(true);
    24              this.State = SessionStates.Aborted;
    25          }                
    26          this.ResponseBodyBytes = this.Response.TakeEntity();
    27          if (this.Response.ServerPipe != null)
    28          {
    29           this.Response.ServerPipe.End();
    30          }  
    31 
    32          if (this.ReturnResponse())   // 将从目标服务器读取的信息返回给客户端                          
    33          {
    34               this.State = SessionStates.Done;
    35          }
    36          else
    37          {
    38               this.State = SessionStates.Aborted;  
    39          }
    40          if (this.Request != null && this.Request.ClientPipe != null)
    41          {
    42               this.Request.ClientPipe.End();
    43          }
    44          this.Response.ReleaseServerPipe();
    45     }
    46 }

    看一下上面有注释的四句,是不是感觉有点熟悉,是的,这正是前面讲过的,代理服务器一次会话的四个步骤。                         

    this.ObtainRequest()   // 获取请求信息 
    this.Response.ResendRequest() // 将请求报文重新包装后转发给目标服务器      
    this.Response.ReadResponse () // 读取从目标服务器返回的信息 
    this.ReturnResponse() // 将从目标服务器读取的信息返回给客户端 

     返回目录

  • 相关阅读:
    Socket 的网络编程
    《Python 3.5从零开始学》笔记-第8章 面向对象编程
    Python 的8个关键要素
    分布式发布订阅模型网络的实现有哪些
    MongoDB知识整理
    C++模板类与Qt信号槽混用
    C++中 =default,=delete用法
    QT知识整理
    Python题整理
    STL库的应用
  • 原文地址:https://www.cnblogs.com/jivi/p/2952883.html
Copyright © 2011-2022 走看看