Internet ExplorerはPOSTが苦手
とある問題が生じて、Internet ExplorerのPOSTについて調査したところ、どうも、IEは、POSTの処理がかなり苦手らしい。ちょっと調べただけでも"Internet Explorer", "POST"で、修正されているものばかりではあるが、下記のような問題が見つかる。
FIX: POST Data Is Truncated When You Call IWebBrowser2::Navigate
Data for a POST Request Is Not Downloaded Completely in a Custom MIME Viewer
Back Navigation on POST Causes Re-POST in Internet Explorer 6
Incorrect Address Passed When Using POST Versus GET Method
POSTがPOST元のページにリダイレクトされる場合の問題
つぎのような仕組みで、POSTがもとのページにリダイレクトされるような仕組みを作成することがある。
#!/usr/bin/perl if($ENV{"REQUEST_METHOD"} eq "POST") { print<<"EOF"; Content-Disposition: inline; filename="test.aaa" Content-Type: application/aaa EOF system("cat test.aaa"); } else { print<<"EOF"; Content-Type: text/html Cache-Control: no-cache <html> <body> <form method="POST" action="test.pl"> <input type="hidden" name="dummy" value="test"> <input type="submit" value="submit"> </form> </body> </html> EOF }
このような場合に、うちの会社で作っていたMIMEタイプハンドラのActiveXが正しくリクエストをハンドリングできなかった。調査したところ、一般的なActiveXでもうまくいかないことが分かった。こういう場合、通常は、ActiveXのSRCプロパティに対して、正しいURLが渡されることを期待するのだが、意味が分からないことに、SRCプロパティには、IEのキャッシュにあるとおぼしきファイルのローカルパスが渡される。さらに、そのファイルは実は存在しない。こういう問題に出くわすのは、何もうちの会社のプラグインに限らず、Flashなどでもそうだったが、Adobe Readerだけはちゃんと動作していた。
POSTのコンテキスト
ActiveXでは、SRCプロパティからURL Monikerというオブジェクトを作成し、それから目的のファイルの内容を取得する。URL Monikerは、"Unique" Resource Locator Monikerというぐらいで、名前の通りならURL Minokerだけでリソースを一意に識別できないといけないのだが、残念ながら、Monikerには、Post時のデータを記憶するような仕組みはない。実際には、Post時に送られたデータを記憶していなければ、目的のファイルを取得することが出来ないのは当たり前だ。
これを説明しているのが、次の記事だ。
SAMPLE: PostMon.exe Demonstrates How to Use URL Moniker to POST Data
これによると、URL Monikerを使ってPOSTをするには、上記の通り、POSTのデータと追加のHTTPヘッダが必要らしい。そして、POSTのデータは、IBindStatusCallback::GetBindInfoで渡し、追加のヘッダに関しては、IHttpNegotiate::BeginningTransactionで渡すということだ。
しかしながら、現状の問題は、SRCプロパティが全く信用できないことと、Post時のデータをどこからも貰えないということだ。リクエスト先のURLに関しては、まぁ、いくらでもあてにするところはあるのだが、Post時のデータはどこから貰えばいいのか。これは大きな問題だ。実際、ここで数日間、悩みまくることになる。
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; }
これでおおむね動くようになる。
久しぶりに大型のポストなので疲れた。