EternalWindows
RPC / リモートファイル
対応するクライアントはこちら

前節で作成したServerFunctionという関数は、クライアントに対して1という値を返すだけであり、 実際の開発ではこのような関数をクライアントに公開しても仕方ありません。 基本的に、クライアントがサーバーに求めるのは、 サーバーのパフォーマンスを利用して処理を高速に行うか、 あるいはサーバーのリソースを有効に活用するかという点にあると思われるため、 今回はサーバー上のファイルをクライアントが読み取れるような関数を実装したいと思います。

サーバー上のファイルをクライアントが読み取るというのは、 サーバーがファイルからデータを読み取り、これをクライアントに返すことを意味します。 つまり、実際にファイルをオープンするのはサーバーの役割になります。 ただし、ここで少し注意しなければならないのは、 このオープン処理をサーバーのアカウントで行うべきではないという点です。 たとえば、サーバーがサービスとして実装されている場合はSYSTEMアカウントとして動作することになりますが、 クライアントからの要求に応答する場合は、そのクライアントのアカウントとしてコードを実行するべきです。 もし、クライアントがファイルの削除要求を出した場合、 サーバーがSYSTEMとして動作していればどのようなファイルでも削除できてしまい危険ですから、 クライアントのアカウントに基づいたアクセスチェックが行われるべきといえます。

上記のように、サーバーがクライアントのアカウントとしてコードを実行するためには、 偽装と呼ばれる仕組みを利用することになります。 サーバーがクライアントに偽装した場合、サーバーの呼び出し側スレッドにクライアントのトークンが割り当てられ、 以後のアクセスチェックにはこのトークンが参照されることになります。 クライアントを偽装するには、RpcImpersonateClientを呼び出します。

RPC_STATUS RPC_ENTRY RpcImpersonateClient(
    RPC_BINDING_HANDLE BindingHandle
);

BindingHandleは、バインディングハンドルを指定します。 NULLを指定しても問題ありません。

サーバーがクライアントからの要求を終え、元のアカウントとして動作したい場合はRpcRevertToSelfを呼び出します。

RPC_STATUS RPC_ENTRY RpcRevertToSelf(void);

この関数に引数はありません。

次に、今回のIDLファイルを指定します。

[
	uuid (7464e52e-3eda-469d-8bed-0c268cd957c9),
	version (1.0)
]
interface sample
{
	const int BUFFERSIZE = 1024;

	typedef [context_handle] void *HREMOTE;
	
	HREMOTE CreateFileRemote([in, string] unsigned char *lpszFileName);
	void ReadFileRemote([in] HREMOTE hRemote, [out] unsigned char lpszBuf[BUFFERSIZE], [in] int nSize);
	void CloseHandleRemote([in] HREMOTE hRemote);
	
	void Shutdown(void);
}

今回のIDLファイルでは、多くのキーワードが使用されています。 最初のconst intは定数の宣言であり、1024をBUFFERSIZEとして定義しています。 次のtypedefは、void型のポインタをHREMOTEという型で定義しています。 HREMOTEは、サーバーで使用する構造体のアドレスを格納しますが、 クライアントからはこのような構造体が見えませんから、 HREMOTEという抽象的な型で表すようにしています。 [context_handle]というキーワードについては後述します。 [in]というキーワードはデータを関数に渡す場合に指定し、 [out]というキーワードはデータを受け取る場合に指定します。 また、文字列を指定する場合は[string]というキーワードを指定します。 可変長データを受け取るバッファを指定する場合は、 lpszBuf[BUFFERSIZE]のようにサイズを指定します。 次に、今回のACFファイルを指定します。

[
	implicit_handle(handle_t hBinding)
]
interface sample
{
}

次に、今回のプログラムを指定します。

#include <windows.h>
#include <rpc.h>
#include "sample_h.h"

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

struct DATA {
	HANDLE hFile;
};
typedef struct DATA DATA;
typedef DATA *LPDATA;

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	if (RpcServerUseProtseqEpW((USHORT *)L"ncacn_np", 0, (USHORT *)L"\\pipe\\sample", NULL) != RPC_S_OK) {
		MessageBox(NULL, TEXT("プロトコルシーケンスの指定に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	RpcServerRegisterIf(sample_v1_0_s_ifspec, NULL, NULL);
	
	if (RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, FALSE) != RPC_S_OK) {
		MessageBox(NULL, TEXT("リッスンに失敗しました。"), NULL, MB_ICONWARNING);
		RpcServerUnregisterIf(NULL, NULL, FALSE);
		return 0;
	}

	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	RpcServerUnregisterIf(NULL, NULL, FALSE);

	return 0;
}

HREMOTE CreateFileRemote(unsigned char *lpszFileName)
{
	LPDATA lpData;

	lpData = (LPDATA)midl_user_allocate(sizeof(DATA));
	
	RpcImpersonateClient(NULL);

	lpData->hFile = CreateFileA((LPSTR)lpszFileName, GENERIC_ALL, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (lpData->hFile == INVALID_HANDLE_VALUE)
		return NULL;
	
	RpcRevertToSelf();

	return (HREMOTE)lpData;
}

void ReadFileRemote(HREMOTE hRemote, unsigned char *lpszBuf, int nSize)
{
	DWORD  dwResult;
	LPDATA lpData = (LPDATA)hRemote;

	ReadFile(lpData->hFile, lpszBuf, nSize, &dwResult, NULL);

}
void CloseHandleRemote(HREMOTE hRemote)
{
	LPDATA lpData = (LPDATA)hRemote;

	if (lpData->hFile != NULL) {
		CloseHandle(lpData->hFile);
		lpData->hFile = NULL;
	}
}

void __RPC_USER HREMOTE_rundown(HREMOTE hRemote)
{
	CloseHandleRemote(hRemote);

	midl_user_free(hRemote);
}

void Shutdown(void)
{
	RpcMgmtStopServerListening(NULL);
}

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len)
{
	return(malloc(len));
}

void __RPC_API midl_user_free(void __RPC_FAR * ptr)
{
	free(ptr);
}

WinMainの処理は、前節と同様にエンドポイントと設定とインターフェースの登録、 そしてクライアントからの要求のリッスンを行っているだけです。 CreateFileRemoteという関数は、クライアントがサーバー上のファイルをオープンする場合に呼び出す関数で、 第1引数はサーバー上におけるファイルパスです。 実装内容は、次のようになっています。

HREMOTE CreateFileRemote(unsigned char *lpszFileName)
{
	LPDATA lpData;

	lpData = (LPDATA)midl_user_allocate(sizeof(DATA));
	
	RpcImpersonateClient(NULL);

	lpData->hFile = CreateFileA((LPSTR)lpszFileName, GENERIC_ALL, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (lpData->hFile == INVALID_HANDLE_VALUE)
		return NULL;
	
	RpcRevertToSelf();

	return (HREMOTE)lpData;
}

まず、midl_user_allocateを呼び出して、DATA構造体のためのメモリを確保します。 この構造体にはサーバー上のファイルを表すハンドルだけが含まれていますが、 その他のメンバ(ファイル名やファイルサイズなど)を含ませおいてもよいでしょう。 次に、RpcImpersonateClientを呼び出して、クライアントに偽装します。 本当にクライアントのアカウントとして動作しているかを確認したい場合は、 GetUserNameを呼び出してみるとよいでしょう。 CreateFileで取得したファイルハンドルはDATA構造体に格納し、 これをHREMOTE型として返します。 このHREMOTE型は、クライアントからすれば、 サーバー上のファイルをオープンしたファイルハンドルのように見えることになります。 ファイルのオープンが終了すれば、元のアカウントとして動作するようRpcRevertToSelfを呼び出します。

ファイルをオープンしたクライアントは、ReadFileRemoteでファイルのデータを読み取り、 不要になったハンドルはCloseHandleRemoteで開放することになります。

void ReadFileRemote(HREMOTE hRemote, unsigned char *lpszBuf, int nSize)
{
	DWORD  dwResult;
	LPDATA lpData = (LPDATA)hRemote;

	ReadFile(lpData->hFile, lpszBuf, nSize, &dwResult, NULL);

}
void CloseHandleRemote(HREMOTE hRemote)
{
	LPDATA lpData = (LPDATA)hRemote;

	if (lpData->hFile != NULL) {
		CloseHandle(lpData->hFile);
		lpData->hFile = NULL;
	}
}

HREMOTE型はDATA構造体へのアドレスを格納していますから、 DATA構造体にキャストしてhFileメンバを参照することができます。 ReadFileの呼び出しではlpszBufが初期化されることになりますが、 これはIDL内で[out]キーワードが指定されているため、 lpszBufに格納されたデータはクライアントに返されることになります。

今回のIDLファイルでは、HREMOTEに[context_handle]というキーワードを指定していますが、 これは必須というわけではありません。 ポインタ変数をコンテキストハンドルとして定義すると、 クライアントの終了時にランダウン関数が自動的に呼ばれるというだけであり、 これが不要な場合は[context_handle]キーワードは不要です。

void __RPC_USER HREMOTE_rundown(HREMOTE hRemote)
{
	CloseHandleRemote(hRemote);

	midl_user_free(hRemote);
}

ランダウン関数の名前は、コンテキストハンドル名_rundownであり、第1引数はコンテキストハンドルです。 ここでCloseHandleRemoteを呼び出しておくと、 たとえクライアントがCloseHandleRemoteを呼び忘れていても、 ファイルが確実にクローズされることになり、便利といえます。 DATA構造体にクライアントの名前を格納している場合は、 クライアントが切断した旨をUIで表示したりするのもよいでしょう。 なお、ランダウン関数はクライアントの終了時に呼ばれますが、 コンテキストハンドルにNULLが格納されている場合は呼ばれることはありません。

クライアント情報の取得

Windows XPからは、RpcServerInqCallAttributesというクライアントの情報を取得する関数が登場しました。 この情報というのは、クライアントのアドレスや認証レベル、プロセスIDなど様々であり、 RPC_CALL_ATTRIBUTES_V2構造体に格納されることになっています。 RPC_CALL_ATTRIBUTES_V1という構造体もありますが、 Windows VistaからはRPC_CALL_ATTRIBUTES_V2構造体を使用することができるため、 こちらを使用するようにします。 次に、RpcServerInqCallAttributesを呼び出す例を示します。

TCHAR               szImageName[MAX_PATH];
DWORD               dwSize;
HANDLE              hProcess;
RPC_CALL_ATTRIBUTES callAttributes;

ZeroMemory(&callAttributes, sizeof(RPC_CALL_ATTRIBUTES));
callAttributes.Version = RPC_CALL_ATTRIBUTES_VERSION;
callAttributes.Flags   = RPC_QUERY_CLIENT_PID;

RpcServerInqCallAttributes(NULL, &callAttributes);

hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, (DWORD)callAttributes.ClientPID);

dwSize = sizeof(szImageName) / sizeof(TCHAR);
QueryFullProcessImageName(hProcess, 0, szImageName, &dwSize);
MessageBox(NULL, szImageName, TEXT("OK"), MB_OK);

CloseHandle(hProcess);

RPC_CALL_ATTRIBUTES構造体のVersionは、RPC_CALL_ATTRIBUTES_VERSIONを指定します。 この定数は、2と定義されているはずです。 Flagsは、どのような情報を取得するかを表す定数を指定します。 今回は、クライアントのプロセスIDを取得するため、RPC_QUERY_CLIENT_PIDを指定しています。 RpcServerInqCallAttributesに第1引数は、バインディングハンドルを指定しますが、 NULLを指定すると現在この関数を呼び出しているクライアントが対象となります。 関数が成功した場合は、ClientPIDにプロセスIDが格納されているため、 これをOpenProcessに指定してプロセスのハンドルを取得します。 そして、QueryFullProcessImageNameを呼び出せば、クライアントのEXEファイルのフルパスを取得することができます。 この関数はWindows Vistaから使用可能な関数ですが、 RPC_CALL_ATTRIBUTES_V2構造体がWindows Vistaから使用可能になっているため、 特に問題はありません。

RpcServerInqCallAttributesでクライアントのプロセスIDを取得する場合は、 通信に使用するプロトコルがLPCになっている必要があります。 よって、サーバーはRpcServerUseProtseqEpを次のように呼び出すことになるでしょう。

RpcServerUseProtseqEp((USHORT *)L"ncalrpc", 0, (USHORT *)L"sample", NULL)

第1引数に、ncalrpcを指定します。 これによって、LPC通信が行われることになります。 第3引数のエンドポイントは、自由に決定して構いません。 クライアントが呼び出すRpcStringBindingComposeでは、上記に指定した値を指定し、 ネットワークアドレスにはNULLを指定します。



戻る