EternalWindows
WebBrowser コントロール / IEとの互換性

ここでは、今回開発したブラウザで使用されてない技術を取り上げます。 IEがサポートしている機能をなるべく自身のブラウザでも使用したい場合は、 以下の情報が役立でるかもしれません。

外部要求の応答

ブラウザが持っている機能を、他のアプリケーションが使用できるようになれば便利であるといえます。 たとえば、IEはDDE(Dynamic Data Exchange)をサポートしているため、特定のURLをDDEによって渡すことで、IEがそのURLにアクセスするようになります。 ただし、DDEはウインドウメッセージをベースとした通信であるため、ブラウザとクライアントの整合性レベルが異なる場合は、 通信が行えなくなる点に注意してください。 ブラウザの方からクライアントにデータを通知したい場合もあるかもしれませんが、これはETW(Event Tracing for Windows)を使用するのがよいと思われます。 ETWを使用するようになれば、イベントの書き込みという操作を行うだけで、クライアントにイベントを一斉に通知できます。 事実、IEはETWをサポートするようになっています。 クライアントによっては、ブラウザを識別するIWebBrowser2を取得したい場合があるかもしれませんが、 これはShellWindowsにブラウザを提供するオブジェクトを登録するのがよいと思われます。 これにより、クライアントはIShellWindows::Itemを通じてオブジェクトを取得し、 そのオブジェクトのIServiceProviderを通じてIWebBrowser2を取得できるようになります。 この方法もIEではサポートされています。 IWebBrowser2を取得する別の方法として、次のようにウインドウハンドルを手掛かりにする方法もあります。

#include <windows.h>
#include <shlobj.h>
#include <oleacc.h>

#pragma comment (lib, "oleacc.lib")

BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam);
BOOL GetWebBrowser2(LPTSTR lpszClassName, IWebBrowser2 **pp);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	BSTR         bstr;
	IWebBrowser2 *pWebBrowser2;
	
	CoInitialize(NULL);

	if (!GetWebBrowser2(TEXT("container"), &pWebBrowser2)) {
		MessageBox(NULL, TEXT("IWebBrowser2の取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	pWebBrowser2->get_LocationURL(&bstr);
	MessageBoxW(NULL, bstr, TEXT("OK"), MB_OK);

	SysFreeString(bstr);
	pWebBrowser2->Release();
	CoUninitialize();

	return 0;
}

BOOL GetWebBrowser2(LPTSTR lpszClassName, IWebBrowser2 **pp)
{
	HWND             hwnd;
	UINT             uMsg;
	LRESULT          lResult;
	HRESULT          hr;
	IServiceProvider *pServiceProvider;

	EnumChildWindows(FindWindow(lpszClassName, NULL), EnumChildProc, (LPARAM)&hwnd);
	if (hwnd == NULL)
		return FALSE;

	uMsg = RegisterWindowMessage(TEXT("WM_HTML_GETOBJECT"));
	if (!SendMessageTimeout(hwnd, uMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, (LPDWORD)&lResult))
		return FALSE;
	
	hr = ObjectFromLresult(lResult, IID_IServiceProvider, 0, (void **)&pServiceProvider);
	if (FAILED(hr))
		return FALSE;
	
	hr = pServiceProvider->QueryService(SID_SWebBrowserApp, IID_PPV_ARGS(pp));
	pServiceProvider->Release();

	return SUCCEEDED(hr);
}

BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam)
{
	TCHAR szClassName[256];

	GetClassName(hwnd, szClassName, sizeof(szClassName) / sizeof(TCHAR));
	if (lstrcmp(szClassName, TEXT("Internet Explorer_Server")) == 0) {
		*((HWND *)lParam) = hwnd;
		return FALSE;
	}
	else
		return TRUE;
}

WebBrowser コントロールを使用しているブラウザでは、"Internet Explorer_Server"というクラス名を持ったウインドウが作成されています。 このウインドウからはIWebBrowser2を取得できるようになっているため、まずはこのウインドウのウインドウハンドルを指定しなければなりません。 GetWebBrowser2の第1引数には今回作成したブラウザのクラス名を指定し、 これを基にブラウザのウインドウを取得した関数は、EnumChildWindowsで子ウインドウを検索します。 ここで"Internet Explorer_Server"が見つかったならば、RegisterWindowMessageとObjectFromLresultを通じてオブジェクトを取得します。 ここで返るオブジェクトは、IHTMLDocument2を実装するドキュメントオブジェクトなのですが、 IServiceProvider::QueryServiceを呼び出すことにより、IWebBrowser2を取得できます。

整合性レベルの設定

アプリケーションの実装に脆弱性がある場合、意図しないコードが実行されてシステムに何らかの影響をもたらす可能性があります。 このような問題を防ぐためには、アプリケーションの実装を正しく行うのはもちろんのことですが、 プロセスの整合性レベルをLowに下げておけば、それだけでかなり完全になります。 システムに存在するファイルやレジストリキーの整合性レベルは、基本的にMedium以上になっているため、 Mediumより低いLowのプロセスからはアクセスが失敗するからです。 UACが有効な状況においてIEは、保護モードという名の下で整合性レベルを低くしますが、 独自のブラウザからはこの保護モードを直接使用することはできません。 BHOやActiveXといったIEにロード可能なDLLは、IEの整合性レベルが低いかをIEIsProtectedModeProcessで判断できますが、 独自のブラウザの整合性レベルが低いかはこの関数で判断できないわけです(E_NOTIMPLが返る)。 よって、独自のブラウザがBHOやActiveXといったDLLを使用する場合は、 整合性レベルを低くすることでこれらのDLLに問題が発生する可能性を意識するべきでしょう。 また、こうしたDLLの問題外にも、整合性レベルを低くすることで特定の機能が失敗することがあります。 たとえば、ソースの表示や履歴の取得などがこれに該当します。 この問題を解決するためには、最初にプロセスが起動された際にもう一度自分自身を起動し、 自身の整合性レベルをLowに設定する処理を行います。 ユーザーが操作するのはこのLowのプロセスであり、最初に起動されたMediumのプロセスは見えないようにしておきます。 そして、Lowのプロセスがセキュリティ的な操作を行くなった場合はMediumのプロセスにそれを通知し、 結果を受け取るようにします。 整合性レベルをLowに設定する処理は、次のようなコードで可能です。

HANDLE                hToken;
TOKEN_MANDATORY_LABEL mandatoryLabel;
PSID                  pSid; 
DWORD                 dwSidSize;

if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &hToken)) {
	MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
	return 0;
}

dwSidSize = SECURITY_MAX_SID_SIZE;
pSid = (PSID)LocalAlloc(LPTR, dwSidSize);
CreateWellKnownSid(WinLowLabelSid, NULL, pSid, &dwSidSize);

mandatoryLabel.Label.Attributes = SE_GROUP_INTEGRITY;
mandatoryLabel.Label.Sid = pSid;

if (!SetTokenInformation(hToken, TokenIntegrityLevel, &mandatoryLabel, sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pSid))) {
	MessageBox(NULL, TEXT("整合性レベルの設定に失敗しました。"), NULL, MB_ICONWARNING);
	LocalFree(pSid);
	CloseHandle(hToken);
	return 0;
}

子プロセスがこうした処理を行わず、親プロセスがプロセスの作成時にこうした処理を行えばよいように思えますが、 これは上手くいきません。 トークンを指定してプロセスを作成する関数には、CreateProcessAsUserとCreateProcessWithTokenWがありますが、 これらを呼び出すには呼び出し側に特定の特権が割り当てられていなければならないからです。 そして、この特権は制限されたユーザーには割り当てられていません。

マルチプロセスとジョブ

ブラウザは、BHOやActiveXといったDLLをアドオンという形でアドレス空間にロードできます。 この仕組みは、ブラウザの動作を拡張するという点で望ましいものですが、一方でエラーの発生を隔離できないという問題もあります。 つまり、DLL内でエラーが発生するとそのDLLをロードしているプロセスが終了してしまうということです。 タブブラウザにおいて、あるタブでエラーが発生したからといって、他のタブも巻き込んでプロセスが終了してしまうのは面白くないため、 IE8ではLoosely-Coupled IEという仕組みによって、1つのタブが1つのプロセスに関連付けられています。 これにより、あるタブでエラーが発生しても、そのタブに関連する1つのプロセスが終了するだけで、他のタブのプロセスに影響は出ません。 複数のプロセスをあたかも1つのプロセスとして見せる方法としては、最初に起動されたプロセスをメインプロセスとして扱う方法が考えられます。 このメインプロセスは、ブラウザとしてのメニューやツールバー、サイドバーを持ちますが、Webページの表示については子プロセス(タブ)に任せるようにします。 つまり、子プロセスが作成したWebページ用のウインドウをメインプロセス上に表示するわけです。 メインプロセスが終了する場合は子プロセスを終了させることになりますが、 もしメインプロセスが強制終了した場合は子プロセスが残ってしまう問題があります。 これを防ぐには、子プロセスをジョブというカーネルオブジェクトに追加しておくとよいでしょう。 そうすれば、ジョブが開放された際にジョブに追加されたプロセスを終了させることができます。 次に、ジョブの使用例を示します。

#include <windows.h>

TCHAR g_szKey[] = TEXT("child-key");
TCHAR g_szJobObjectName[] = TEXT("job");

void CreateChildProcess(LPTSTR lpszKey);
LRESULT CALLBACK MainProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK ChildProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;
	WNDPROC    lpfnWndProc;
	
	if (lstrcmp(GetCommandLine(), g_szKey) == 0)
		lpfnWndProc = ChildProc;
	else
		lpfnWndProc = MainProc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = lpfnWndProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK MainProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HANDLE hJob = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedLimit;
		
		hJob = CreateJobObject(NULL, g_szJobObjectName);

		ZeroMemory(&extendedLimit, sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
		extendedLimit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
		SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &extendedLimit, sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
		
		SetWindowText(hwnd, TEXT("main"));

		return 0;
	}

	case WM_LBUTTONDOWN:
		CreateChildProcess(g_szKey);
		return 0;

	case WM_DESTROY:		
		CloseHandle(hJob);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

LRESULT CALLBACK ChildProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg) {

	case WM_CREATE: {
		HANDLE hJob;

		hJob = OpenJobObject(JOB_OBJECT_ASSIGN_PROCESS, FALSE, g_szJobObjectName);
		
		AssignProcessToJobObject(hJob, GetCurrentProcess());
		
		CloseHandle(hJob);

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void CreateChildProcess(LPTSTR lpszKey)
{
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;
	TCHAR               szModuleName[MAX_PATH];

	GetModuleFileName(NULL, szModuleName, MAX_PATH);

	startupInfo.dwFlags = 0;
	GetStartupInfo(&startupInfo);
	CreateProcess(szModuleName, lpszKey, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInformation);

	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
}

コマンドライン文字列として"child-key"が渡された場合は、このプロセスが子プロセスとして起動されたことを意味します。 この場合は、ウインドウプロシージャを子プロセス用に設定し、 WM_CREATEではメインプロセスが作成したジョブをOpenJobObjectでオープンします。 そして、AssignProcessToJobObjectで自プロセスをジョブに追加します。 一方、"child-key"が渡されなかった場合はプロセスが初めて起動されたということで、 このプロセスをメインプロセスとして扱うようにします。 WM_CREATEではCreateJobObjectでジョブを作成し、 子プロセスのOpenJobObjectが成功するようにしておきます。 プロセスが強制終了された場合はジョブの参照カウントが0になりますが、 このときJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEが設定されていれば、 ジョブに追加されたプロセスが破棄されるようになります。 実際に、SetInformationJobObjectをコメントアウトしてメインプロセスを強制終了すると、 子プロセスが残ることが分かります。 マウスの左ボタンが押された場合は、CreateChildProcessで子プロセスを作成します。 このとき、コマンドライン文字列として"child-key"を渡す点が重要です。 メインプロセスのウインドウを閉じた際にはCloseHandleを呼び出していますが、 これによってジョブに追加された子プロセスは一斉に終了します。 本来はこの処理はTerminateJobObjectで行いますが、 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEが設定されている場合は、CloseHandleで可能です。 なお、上記コードに整合性レベルの処理を取り入れるならば、 子プロセスの初期化段階で整合性レベルを下げる処理を追加することになるでしょう。 これにより、メインプロセスの整合性レベルはMediumのままであり、子プロセスの整合性レベルはLowになります。


戻る