Browser Helper Object

そしてそんな僕の前に現れた希望は、Browser Helper Object (BHO)という奴だった(詳細については、Building Browser Helper Objects with Visual Studio 2005がわかりやすい)。名前だけはマルウェアで使われまくったおかげで非常に有名になった奴だ。BHOは、IEが起動時にロードされ、IEの機能を文字通り助けるオブジェクトだ。このオブジェクトは、典型的には、IEの各イベントを受け取り、それに応じて、何らかの処理を行う。そして、僕が発見した、POSTの問題を解決してくれそうなイベントはDWebBrowserEvents2::BeforeNavigate2だ。

void BeforeNavigate2(
    IDispatch *pDisp,
    VARIANT *url,
    VARIANT *Flags,
    VARIANT *TargetFrameName,
    VARIANT *PostData,
    VARIANT *Headers,
    VARIANT_BOOL *Cancel);

ここには、探し求めていたPostDataがある。さらに、pDispの説明には、

[in] Pointer to the IDispatch interface for the WebBrowser object that represents the window or frame. This interface can be queried for the IWebBrowser2 interface.

とある。さらに、IWebBrowser2::PutProperty/GetPropertyが僕の背中を後押しする。

BeforeNavigate2で取得したPostDataやHeadersをどうにかまとめて、IWebBrowser2::PutPropertyでプロパティとして登録して、ActiveX側でIWebBrowser2::GetPropertyすればいいのではないか。

とりあえず、やってみよう。最初に、IWebBrowser2::PutPropertyでプロパティとして登録するには、VARIANT型でなくてはならないので、これは、IUnknown*か、BSTRにするのが筋だろうか。どちらでもいいのだけれども、本当は、IWebBrowser2::PutPropertyは、VT_BSTRのVARIANTしか受け付けないことになっている。試しに、いろいろと食べさせてみたが、どうやら、IUnknown*は行けるようだ。あえて冒険をする必要はないが、BSTRにするにはいろいろと面倒そうなので、とりあえず、IUnknown*で行くことにする。最初に、そのオブジェクトを作ろう。

class PostData : public IUnknown
{
public:
  // 嘘COMなので、取扱注意
  PostData() : m_refCo(0) {}
  virtual ~PostData() {}

  // ほとんど構造体なので、あえてpublic
  CComVariant m_url;
  CComVariant m_postData;
  CComVariant m_headers;

  // IUnknownの真似事
  virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
  {
    if(riid == IID_IUnknown) // ほかのインターフェイスはない
    {
      *ppvObject = this;
      return S_OK;
    }
    return E_FAIL;
  }

  virtual ULONG STDMETHODCALLTYPE AddRef(void)
  {
    return (ULONG)InterlockedIncrement(&m_refCo);
  }

  virtual ULONG STDMETHODCALLTYPE Release(void)
  {
    LONG co = InterlockedDecrement(&m_refCo);
    if(co == 0) delete this;
    return (ULONG)co;
  }

private:
  LONG m_refCo;
};

このオブジェクトを作成して、そこにデータを格納するのは、DWebBrowserEvents2::BeforeNavigate2。

void __stdcall OnBeforeNavigate2(
  IDispatch* pDisp,
  VARIANT* pvUrl,
  VARIANT* pvFlags,
  VARIANT* pvTarget,
  VARIANT* pvPostData,
  VARIANT* pvHeaders,
  VARIANT_BOOL *pbCancel)
{
  if(!isVariantEmpty(pvPostData))
  {
    CComPtr<PostData> postData = new PostData(); // 嘘COM

    // 必要な情報を全部覚える
    postData->m_url = *pvUrl;
    postData->m_headers = *pvHeaders;

    // pvPostDataは、VT_BYREFであるので、参照を外す
    if(pvPostData->vt == (VT_VARIANT | VT_BYREF))
      pvPostData = pvPostData->pvarVal;
    postData->m_postData = *pvPostData;


    // pDispからIWebBrowser2を取得する
    CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> pWebBrowser = (IUnknown*)pDisp;
    
    // PostDataをVARIANT型でプロパティとして記録
    CComVariant v(postData);
    CComBSTR prop(_T("AxTest_BrowserHelper_PostData"));
    pWebBrowser->PutProperty(prop, v);
  }
}

ここで、isVariantEmptyは、とりあえず、適当にVARIANTが空(or 空文字列)かどうかをチェックする関数。次のように実装してみた。

static bool isVariantEmpty(const VARIANT* v)
{
  if(!v) return true;
  if(v->vt & VT_BYREF) // リファレンスならリファレンスを外す
    return isVariantEmpty(v->pvarVal);

  VARTYPE prefix = v->vt & ~VT_TYPEMASK;
  if(prefix && v->byref == NULL)
    return true;

  VARTYPE type = v->vt & VT_TYPEMASK;
  if(type == VT_EMPTY || type == VT_NULL || type == VT_ERROR)
    return true;
  return false;
}

そうすると、いよいよActiveX側でこの情報を取得できることになる。これに関しては、どこでやっても良いのだろうけど、とりあえず、put_SRCの中でやってみようと思うのだけれども、クライアント側で使うIWebBrowser2のインスタンスには注意が必要。IServiceProviderから直接取得した物でないと、さっきプロパティを設定したものと別物のインスタンスになってしまう。IOleObject::SetClientSiteで行うなら次の様になる(下記は、ATLのIOleObjectImplを使った場合の例)。

HRESULT IOleObject_SetClientSite(IOleClientSite *pClientSite)
{
  HRESULT hr = CComControlBase::IOleObject_SetClientSite(pClientSite);
  if(FAILED(hr))
    return hr;

  m_pSP.Release();
  m_pBrowser.Release();

  if(!pClientSite)
    return E_POINTER;

  // m_pSPは、CComPtr<IID_IServiceProvider>
  if(FAILED(pClientSite->QueryInterface(IID_IServiceProvider, (void**)&m_pSP)))
    return hr;

  // m_pBrowserは、CComPtr<IID_IWebBrowser2>
  if(FAILED(m_pSP->QueryService(IID_IWebBrowserApp, IID_IWebBrowser2, (void**)&m_pBrowser)))
    return hr;

  return S_OK;
}

ここまでのお膳立てができれば、put_SRCは、次のようになるはず。

STDMETHOD(put_SRC)(BSTR newVal)
{
  m_src = newVal;

  CComBSTR prop(_T("AxTest_BrowserHelper_PostData"));
  CComVariant v;
  if(SUCCEEDED(m_pBrowser->GetProperty(prop, &v)) && v.vt == VT_UNKNOWN && v.punkVal)
  {
    // POSTデータの取り出し
    PostData* pd = dynamic_cast<PostData*>(v.punkVal);

    // URLの修正
    m_src = pd->m_src;

    // IBindStatusCallbackにPOSTデータを渡す
    CBindStatusCallback_T<CAxTestCtrl>::Create(pd, (void**)&m_bsCallback);

    // POSTデータはもういらないので、消しておく
    v.ClearToZero();
    m_pBrowser->PutProperty(prop, v);
  }
  else
  {
    // GETなので、IBindStatusCallbackをPOSTデータなしで作成する
    CBindStatusCallback_T<CAxTestCtrl>::Create(NULL, (void**)&m_bsCallback);
  }

  // ダウンロード開始
  m_bsCallback->StartAsyncDownload(this, OnDataAvail, m_src, m_spClientSite, FALSE);
  return S_OK;
}

CBindStatusCallback_Tは、適当に作ればいいのだけれども、キモの部分は、IBindStatusCallbackだけではなく、IHttpNegotiateも継承すべきであること。これは、上記のPostMon.exeで示されているとおり。

そして、POSTのデータは、IBindStatusCallback::GetBindInfoで設定する。

STDMETHOD(GetBindInfo)(DWORD *pgrfBINDF, BINDINFO *pbindInfo)
{
  if(!pbindInfo || pbindInfo->cbSize == 0 || !pgrfBINDF)
    return E_INVALIDARG;
  
  ULONG cbSize = pbindInfo->cbSize;
  ZeroMemory(pbindInfo, cbSize);
  pbindInfo->cbSize = cbSize;
  if(m_hMem) GlobalFree(m_hMem); // 再入する可能性があるので注意

  *pgrfBINDF = BINDF_ASYNCHRONOUS | BINDF_ASYNCSTORAGE | BINDF_GETNEWESTVERSION;

  if(m_postData)
  {
    // POSTの場合
    pbindInfo->dwBindVerb = BINDVERB_POST;

    // SAFEARRAYでデータが来るので、それをメモリに展開する
    const VARIANT&v = m_postData;
    if(v.vt == (VT_UI1 | VT_ARRAY) && v.parray && v.parray->cDims == 1)
    {
      SIZE_T size = v.parray->cbElements * v.parray->rgsabound[0].cElements;
      const void* p = v.parray->pvData;
      m_hMem = GlobalAlloc(GPTR, size);
      CopyMemory((void*)m_hMem, p, size);

      pbindInfo->stgmedData.tymed = TYMED_HGLOBAL;
      pbindInfo->cbstgmedData = size;
      pbindInfo->stgmedData.hGlobal = m_hMem;

      // m_hMemを解放するのは、このインスタンスの責任なので、自分をAddRefしておく。
      // Releaseは、システムが行ってくれるらしい。
      pbindInfo->stgmedData.pUnkForRelease = (LPUNKNOWN)(LPBINDSTATUSCALLBACK)this;
      AddRef();
    }
  }
  else
  {
    // GETの場合
    pbindInfo->dwBindVerb = BINDVERB_GET;
  }
  return S_OK;
}

そして、追加のヘッダを処理するのが、IHttpNegotiate::BeginningTransactionで、これはだいたい次のようになる。

STDMETHOD(BeginningTransaction)(
  LPCWSTR szURL,
  LPCWSTR szHeaders,
  DWORD dwReserved,
  LPWSTR* pszAdditionalHeaders)
{
  if(!pszAdditionalHeaders)
    return E_POINTER;
  *pszAdditionalHeaders = NULL;
  if(m_postData)
  {
    // Add headers
    const VARIANT& v = m_headers;
    if(v.vt == VT_BSTR)
    {
      LPWSTR wszAdditionalHeaders = (LPWSTR)CoTaskMemAlloc((wcslen(v.bstrVal) + 1) *sizeof(WCHAR));
      wcscpy(wszAdditionalHeaders, v.bstrVal);
      *pszAdditionalHeaders = wszAdditionalHeaders;
    }
  }
  return NOERROR;
}

これでおおむね動くようになる。
久しぶりに大型のポストなので疲れた。