Search code examples
delphichromium-embedded

blob urls and cross-origin errors with DCEF GetResourceHandler


I've implemented a GetResourceHandler call in Delphi 2007 using the latest Delphi Chromium Embedded wrapper (which uses libcef 3.1750.1738, I believe compiled by the DCEF3 creator). I based my handler on some python code posted on the magpcss site (with some changes).

All is working well, except for a couple of things. First off, "blob:" urls don't work and I'm scratching my head as to how to get them to. I'm not even that knowledgeable about them in general so that's part of the problem.

Second, some cross-origin stuff pops up when using my resource handler that otherwise doesn't. This is in my tests against https://maps.google.com/. I get:

GET blob:https%3A//map.google.com/93e08d0a-b8b3-4c60-8a82-71d424b0893c 404 (Not Found) rs=ACT90oHHUBc59MzTbg1AU48l7w-F8f1yuA:1107
GET blob:https%3A//map.google.com/4c89a387-5958-4c19-ae9c-0640007db009 404 (Not Found) rs=ACT90oHHUBc59MzTbg1AU48l7w-F8f1yuA:1107
GET blob:https%3A//map.google.com/bd839949-a77b-43a1-9f47-1cb841f858a9 404 (Not Found) /maps/_/js/k=maps.m.en.oD0-LnUwxmc.O/m=sy437,sy444,sy446,sy486,sy494,wrc,sy438,sy436,vw,sy139,sy220…:134
XMLHttpRequest cannot load https://mt0.google.com/vt?pb=!1m8!4m7!2u15!5m2!1x450831026!2x3361961734!6m2!1x451029165!2x3362284457!2m1!1e0!3m1!5e1105!4e5!18m1!1b1. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://map.google.com' is therefore not allowed access. /maps/@45.0930104,-93.28442,15z:1
XMLHttpRequest cannot load https://mt0.google.com/vt/pb=!1m8!3m7!1m2!1u2020096!2u3014144!2m2!1u1024!2u768!3i15!2m3!1e0!2sm!3i290!3m2!2sen!5e1105!4e4!11m2!1e2!2b1!20m1!1b1. No 'Access-Control-Allow-    Origin' header is present on the requested resource. Origin 'https://map.google.com' is therefore not allowed access. /maps/@45.0930104,-93.28442,15z:1

Yet neither of those problems happen when omitting my resource handler and using CEF without it. I've found I can get around the CORS problems by disabling web security. That hardly seems ideal and, again, isn't needed when using CEF without my resource handler.

Here's my resource handler code:

unit WebInterceptHandler;

interface

uses
  ceflib, Classes;

type
  TWebInterceptHandler = class(TCefResourceHandlerOwn)
  protected
    FDataStream: TMemoryStream;
    FResponseHeadersReadyCallback: ICefCallback;
    FOffsetRead: NativeUInt;
    FResponse: ICefResponse;

    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(const Browser: ICefBrowser; const Frame: ICefFrame;
        const SchemeName: ustring; const Request: ICefRequest); override;
    destructor Destroy; override;
  end;

implementation

uses windows, sysutils;

type
  TWebInterceptHandlerClient = class(TCefUrlRequestClientOwn)
  private
    FResourceHandler: TWebInterceptHandler;
  protected
    procedure OnDownloadData(const Request: ICefUrlRequest; Data: Pointer;
        DataLength: NativeUInt); override;
    procedure OnRequestComplete(const Request: ICefUrlRequest); override;
  end;

{ TWebInterceptHandlerClient }

procedure TWebInterceptHandlerClient.OnDownloadData(const Request: ICefUrlRequest;
  Data: Pointer; DataLength: NativeUInt);
begin
  inherited;

  FResourceHandler.FDataStream.Write(Data^, DataLength);

  //OutputDebugStringW(PWideChar(Request.GetRequest.Url));
end;

procedure TWebInterceptHandlerClient.OnRequestComplete(const Request: ICefUrlRequest);
begin
  inherited;

  FResourceHandler.FResponse := Request.GetResponse;
  if FResourceHandler.FResponseHeadersReadyCallback <> nil then
    FResourceHandler.FResponseHeadersReadyCallback.Cont;
end;


{ TWebInterceptHandler }

constructor TWebInterceptHandler.Create(const Browser: ICefBrowser;
  const Frame: ICefFrame; const SchemeName: ustring;
  const Request: ICefRequest);
begin
  inherited;

  FDataStream := TMemoryStream.Create;
end;

//function HTTPDecode(const AStr: ustring): rbstring;
//var
//  Sp, Rp, Cp: PAnsiChar;
//  src: rbstring;
//begin
//  src := rbstring(AStr);
//  SetLength(Result, Length(src));
//  Sp := PAnsiChar(src);
//  Rp := PAnsiChar(Result);
//  while Sp^ <> #0 do
//  begin
//    case Sp^ of
//      '+': Rp^ := ' ';
//      '%': begin
//             Inc(Sp);
//             if Sp^ = '%' then
//               Rp^ := '%'
//             else
//             begin
//               Cp := Sp;
//               Inc(Sp);
//               if (Cp^ <> #0) and (Sp^ <> #0) then
//                 Rp^ := AnsiChar(StrToInt('$' + Char(Cp^) + Char(Sp^)))
//               else
//               begin
//                 Result := '';
//                 Exit;
//               end;
//             end;
//           end;
//    else
//      Rp^ := Sp^;
//    end;
//    Inc(Rp);
//    Inc(Sp);
//  end;
//  SetLength(Result, Rp - PAnsiChar(Result));
//end;

destructor TWebInterceptHandler.Destroy;
begin
  FDataStream.Free;

  inherited;
end;

function TWebInterceptHandler.ProcessRequest(const Request: ICefRequest;
      const Callback: ICefCallback): Boolean;
var
  wrc: TWebInterceptHandlerClient;
//  url: ustring;
//  i: Integer;
//  headerMap: ICefStringMultimap;
begin
  //headerMap := TCefStringMultimapOwn.Create;

  //OutputDebugStringW(PWideChar(Request.Url));

  //Request.GetHeaderMap(headerMap);
  //if headerMap.Size <> 0 then
  //  for i := 0 to headerMap.Size - 1 do
  //    OutputDebugStringW(PWideChar(headerMap.Key[i] + ': ' + headerMap.Value[i]));

  //if Pos('blob:', WideLowerCase(Request.Url)) = 1 then
  //  request.Url := HTTPDecode(Copy(Request.Url, 1, Length(Request.Url) - Length('blob:')));

  FOffsetRead := 0;

  FResponseHeadersReadyCallback := Callback;
  wrc := TWebInterceptHandlerClient.Create;
  wrc.FResourceHandler := Self;
  TCefUrlRequestRef.New(request, wrc);

  Result := True;
end;

procedure TWebInterceptHandler.GetResponseHeaders(const Response: ICefResponse;
    out ResponseLength: Int64; out RedirectUrl: ustring);
var
  headerMap: ICefStringMultimap;
begin
  headerMap := TCefStringMultimapOwn.Create;
  Response.Status := FResponse.Status;
  Response.StatusText := FResponse.StatusText;
  Response.MimeType := FResponse.MimeType;

  FResponse.GetHeaderMap(headerMap);
  if headerMap.Size <> 0 then
    FResponse.SetHeaderMap(headerMap);

  ResponseLength := FDataStream.Size;
end;

function TWebInterceptHandler.ReadResponse(const DataOut: Pointer; BytesToRead:
    Integer; var BytesRead: Integer; const Callback: ICefCallback): Boolean;
begin
  if FOffsetRead < FDataStream.Size then
  begin
    BytesRead := BytesToRead;

    Move(Pointer(NativeUInt(FDataStream.Memory) + FOffsetRead)^, DataOut^,
        BytesRead);

    Inc(FOffsetRead, BytesRead);

    Result := True;
  end
  else
    Result := False;
end;

end.

Which I'm calling from a test app:

object MainForm: TMainForm
  Left = 0
  Top = 0
  Caption = 'MainForm'
  ClientHeight = 716
  ClientWidth = 752
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  PixelsPerInch = 96
  TextHeight = 13
  object URLBox: TEdit
    Left = 0
    Top = 0
    Width = 752
    Height = 21
    Align = alTop
    TabOrder = 0
    Text = 'https://map.google.com/'
    OnKeyPress = URLBoxKeyPress
  end
  object Panel1: TPanel
    Left = 0
    Top = 21
    Width = 752
    Height = 41
    Align = alTop
    BevelOuter = bvNone
    TabOrder = 1
    ExplicitLeft = 296
    ExplicitTop = 360
    ExplicitWidth = 185
    object Button1: TButton
      Left = 8
      Top = 8
      Width = 120
      Height = 25
      Caption = 'Show Dev Tools'
      TabOrder = 0
      OnClick = Button1Click
    end
    object UseResourceHandlerBox: TCheckBox
      Left = 152
      Top = 12
      Width = 233
      Height = 17
      Caption = 'Use Resource Handler'
      TabOrder = 1
    end
  end
end


unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, cefgui, ceflib, cefvcl, StdCtrls, ExtCtrls;

type
  TMainForm = class(TForm)
    URLBox: TEdit;
    Panel1: TPanel;
    Button1: TButton;
    UseResourceHandlerBox: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure URLBoxKeyPress(Sender: TObject; var Key: Char);
    procedure Button1Click(Sender: TObject);
  private
    browser: TChromium;
    procedure BrowserGetResourceHandler(Sender: TObject;
        const Browser: ICefBrowser; const Frame: ICefFrame;
        const Request: ICefRequest; out Result: ICefResourceHandler);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses
  WebInterceptHandler;

procedure TMainForm.URLBoxKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then
  begin
    if UseResourceHandlerBox.Checked then
      browser.OnGetResourceHandler := BrowserGetResourceHandler
    else
      browser.OnGetResourceHandler := nil;

    browser.Load(URLBox.Text);
  end;
end;

procedure TMainForm.Button1Click(Sender: TObject);
begin
  browser.ShowDevTools;
end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  browser := TChromium.Create(Self);
  browser.Align := alClient;
  browser.Parent := Self;
  //browser.Options.WebSecurity := STATE_DISABLED;
end;

procedure TMainForm.BrowserGetResourceHandler(Sender: TObject; const Browser:
    ICefBrowser; const Frame: ICefFrame; const Request: ICefRequest;
    out Result: ICefResourceHandler);
begin
  Result := TWebInterceptHandler.Create(Browser, Frame, 'webintercept', Request);
end;

end.

program DCEF3WebIntercept;

uses
  ceflib,
  Forms,
  MainUnit in 'MainUnit.pas' {MainForm},
  WebInterceptHandler in 'WebInterceptHandler.pas';

{$R *.res}

begin
  CefCache := 'cache';
  CefSingleProcess := False;
  if not CefLoadLibDefault then
    Exit;

  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TMainForm, MainForm);
  Application.Run;
end.

Along with any help on those problems, any suggestions for improvements to this code are welcome.

Also, in case anyone is wondering, I'm planning on later modifying this code so I can alter the response. Right now it's just a proof-of-concept of GetResourceHandler.


Solution

  • A blob url is a special url that refers to data that your browser currently has in memory, for the current page. Looks to me that in BrowserGetResourceHandler() you should detect if url starts with "blob:" and return NULL in such case and let Chromium handle fetching that url internally. If it still doesn't work, it may be a limitation in CEF and bug should be reported.

    The CORS security problem seems to be caused by missing headers in request/response. Do some debugging:

    1. Check headers in Google Chrome for the same request
    2. In TWebInterceptHandler.GetResponseHeaders check headers in the headerMap argument and ensure that they match headers in Google Chrome
    3. Make sure FResponse.SetHeaderMap() is being called and check that headers were successfully set on the response - call FResponse.GetHeaderMap and compare it with the headerMap argument