如何检索 XHR 响应?

How to retrieve an XHR response?

基本上,我使用 Chromium 来显示我无法控制的网站。当用户点击特定按钮时,发送 XHR 请求;我希望能够检索到答案。因此,假设请求发送 A,服务器回复 B;我想读B。我该怎么做?

缺少 OnResourceResponse 事件

在旧的 CEF1 中,您可以简单地使用 OnResourceResponse 事件。在 CEF3 中,这样一个看似微不足道的任务可能会成为一个真正的挑战,因为 Issue 515 linked in the answer to the fundamentally same question as you're asking here is still opened and it seems the only way (at this time) is implementing your own CefResourceHandler handler to make a proxy between the browser and the world outside. The implementation principle is described in a similar topic 比如:

You can use CefResourceHandler via CefRequestHandler::GetResourceHandler and execute the request/return the response contents yourself using CefURLRequest.

所以这是在 DCEF3 中要做的(此时):

1。定义您自己的资源处理程序

作为第一个导出你自己的 TCefResourceHandlerOwn 后代,至少要实现以下方法:

  • ProcessRequest - 这里你将转发(发送)请求A到服务器并接收服务器的响应B(这是你第一次机会使用响应数据 B),您应该将其存储(最好在 class 字段中,以便可以轻松地将其刷新到 ReadResponse 方法的输出缓冲区)。

  • GetResponseHeaders - in this method you'll need to fill the output parameters about the response B data length and (some of) the header fields (this might be the place where you will need to have your response B parsed for headers to fill the CefResponse类型参数成员).

  • ReadResponse - 这是您刷新响应 B 数据以供浏览器进行最终处理的地方。

2。将您的资源处理程序分配给请求

下一步是为请求分配您自己的资源处理程序。这在技术上就像从 Chromium 的 OnGetResourceHandler 事件处理程序返回对资源处理程序接口的引用一样简单。但是在这一步中,您应该考虑的是,您越缩小标准,您在资源处理程序中的生活就会越简单。因为如果你分配你的处理程序,例如对于任何请求(对于任何 URL),那么您可能必须处理和过滤掉来自与您的总体任务完全无关的服务器的响应。

因此,我建议将分配范围缩小到由该 Web 应用程序按钮生成的请求,或者至少按请求资源类型(在本例中为 RT_XHR),这样您就不会需要自己处理所有的请求。

代码示例

实现上述步骤的方法有很多种。在这个例子中,我插入了 Chromium 浏览器 class 并在那里添加了一个新事件 OnXmlHttpExchange 当 XHR 请求完成时触发(但在它被传递到浏览器之前,它允许您甚至修改响应) .

uses
  CefLib, CefVCL;

type
  TXmlHttpExchangeEvent = procedure(Sender: TObject; const Request: ICefRequest; const Response: ICefResponse; DataStream: TMemoryStream) of object;

  TChromium = class(CefVCL.TChromium)
  private
    FOnXmlHttpExchange: TXmlHttpExchangeEvent;
  protected
    procedure DoXmlHttpExchange(const Request: ICefRequest; const Response: ICefResponse; DataStream: TMemoryStream); virtual;
    function doOnGetResourceHandler(const Browser: ICefBrowser; const Frame: ICefFrame; const Request: ICefRequest): ICefResourceHandler; override;
  public
    property OnXmlHttpExchange: TXmlHttpExchangeEvent read FOnXmlHttpExchange write FOnXmlHttpExchange;
  end;

  TXmlHttpHandler = class(TCefResourceHandlerOwn)
  private
    FOwner: TChromium;
    FOffset: NativeUInt;
    FStream: TMemoryStream;
    FCallback: ICefCallback;
    FResponse: ICefResponse;
  protected
    function ProcessRequest(const Request: ICefRequest; const Callback: ICefCallback): Boolean; override;
    procedure GetResponseHeaders(const Response: ICefResponse; out ResponseLength: Int64; out RedirectUrl: ustring); override;
    function ReadResponse(const DataOut: Pointer; BytesToRead: Integer; var BytesRead: Integer; const Callback: ICefCallback): Boolean; override;
  public
    constructor Create(Owner: TChromium; const Browser: ICefBrowser; const Frame: ICefFrame; const SchemeName: ustring; const Request: ICefRequest); reintroduce;
    destructor Destroy; override;
    procedure WriteResponse(const Request: ICefUrlRequest; Data: Pointer; Size: NativeUInt); virtual;
    procedure CompleteRequest(const Request: ICefUrlRequest); virtual;
  end;

  TXmlHttpRequestClient = class(TCefUrlrequestClientOwn)
  private
    FHandler: TXmlHttpHandler;
  protected
    procedure OnDownloadData(const Request: ICefUrlRequest; Data: Pointer; DataLength: NativeUInt); override;
    procedure OnRequestComplete(const Request: ICefUrlRequest); override;
  public
    constructor Create(Handler: TXmlHttpHandler); reintroduce;
  end;

implementation

{ TChromium }

procedure TChromium.DoXmlHttpExchange(const Request: ICefRequest; const Response: ICefResponse; DataStream: TMemoryStream);
begin
  // fire the OnXmlHttpExchange event
  if Assigned(FOnXmlHttpExchange) then
    FOnXmlHttpExchange(Self, Request, Response, DataStream);
end;

function TChromium.doOnGetResourceHandler(const Browser: ICefBrowser; const Frame: ICefFrame; const Request: ICefRequest): ICefResourceHandler;
begin
  // first trigger the browser's OnGetResourceHandler event
  Result := inherited;
  // if no handler was assigned and request is of type XHR, create our custom one
  if not Assigned(Result) and (Request.ResourceType = RT_XHR) then
    Result := TXmlHttpHandler.Create(Self, Browser, Frame, 'XhrIntercept', Request);
end;

{ TXmlHttpHandler }

constructor TXmlHttpHandler.Create(Owner: TChromium; const Browser: ICefBrowser; const Frame: ICefFrame; const SchemeName: ustring; const Request: ICefRequest);
begin
  inherited Create(Browser, Frame, SchemeName, Request);
  FOwner := Owner;
  FStream := TMemoryStream.Create;
end;

destructor TXmlHttpHandler.Destroy;
begin
  FStream.Free;
  inherited;
end;

function TXmlHttpHandler.ProcessRequest(const Request: ICefRequest; const Callback: ICefCallback): Boolean;
begin
  Result := True;
  // reset the offset value
  FOffset := 0;
  // store the callback reference
  FCallback := Callback;
  // create the URL request that will perform actual data exchange (you can replace
  // it with any other; e.g. with MSXML, or an Indy client)
  TCefUrlRequestRef.New(Request, TXmlHttpRequestClient.Create(Self));
end;

procedure TXmlHttpHandler.GetResponseHeaders(const Response: ICefResponse; out ResponseLength: Int64; out RedirectUrl: ustring);
var
  HeaderMap: ICefStringMultimap;
begin
  // return the size of the data we have in the response stream
  ResponseLength := FStream.Size;
  // fill the header fields from the response returned by the URL request
  Response.Status := FResponse.Status;
  Response.StatusText := FResponse.StatusText;
  Response.MimeType := FResponse.MimeType;
  // copy the header map from the response returned by the URL request
  HeaderMap := TCefStringMultimapOwn.Create;
  FResponse.GetHeaderMap(HeaderMap);
  if HeaderMap.Size <> 0 then
    FResponse.SetHeaderMap(HeaderMap);
end;

function TXmlHttpHandler.ReadResponse(const DataOut: Pointer; BytesToRead: Integer; var BytesRead: Integer; const Callback: ICefCallback): Boolean;
begin
  // since this method can be called multiple times (reading in chunks), check if we
  // have still something to transfer
  if FOffset < FStream.Size then
  begin
    Result := True;
    BytesRead := BytesToRead;
    // copy the data from the response stream to the browser buffer
    Move(Pointer(NativeUInt(FStream.Memory) + FOffset)^, DataOut^, BytesRead);
    // increment the offset by the amount of data we just copied
    Inc(FOffset, BytesRead);
  end
  else
    Result := False;
end;

procedure TXmlHttpHandler.WriteResponse(const Request: ICefUrlRequest; Data: Pointer; Size: NativeUInt);
begin
  // write the just downloaded data to the intermediate response stream
  FStream.Write(Data^, Size);
end;

procedure TXmlHttpHandler.CompleteRequest(const Request: ICefUrlRequest);
begin
  FStream.Position := 0;
  // store the response reference for the GetResponseHeaders method
  FResponse := Request.GetResponse;
  // this method is executed when the URL request completes, so we have everything we
  // need to trigger the OnXmlHttpExchange event
  FOwner.DoXmlHttpExchange(Request.GetRequest, FResponse, FStream);
  // this signals the handler that the request has completed and that it can process
  // the response headers and pass the content to the browser
  if Assigned(FCallback) then
    FCallback.Cont;
end;

{ TXmlHttpRequestClient }

constructor TXmlHttpRequestClient.Create(Handler: TXmlHttpHandler);
begin
  inherited Create;
  FHandler := Handler;
end;

procedure TXmlHttpRequestClient.OnDownloadData(const Request: ICefUrlRequest; Data: Pointer; DataLength: NativeUInt);
begin
  FHandler.WriteResponse(Request, Data, DataLength);
end;

procedure TXmlHttpRequestClient.OnRequestComplete(const Request: ICefUrlRequest);
begin
  FHandler.CompleteRequest(Request);
end;

和这个插入的可能用法 class:

type
  TForm1 = class(TForm)
    Chromium: TChromium;
    procedure FormCreate(Sender: TObject);
  private
    procedure XmlHttpExchange(Sender: TObject; const Request: ICefRequest; const Response: ICefResponse; DataStream: TMemoryStream);
  end;

implementation

procedure TForm1.FormCreate(Sender: TObject);
begin
  Chromium.OnXmlHttpExchange := XmlHttpExchange;
end;

procedure TForm1.XmlHttpExchange(Sender: TObject; const Request: ICefRequest; const Response: ICefResponse; DataStream: TMemoryStream);
begin
  // here you can find the request for which you want to read the response (explore the
  // Request parameter members to see by what you can do this)
  if Request.Url = 'http://example.com' then
  begin
    // the DataStream stream contains the response, so process it here as you wish; you
    // can even modify it here if you want
    DataStream.SaveToFile(...);
  end;
end;