Смекни!
smekni.com

Перехват методов COM интерфейсов (стр. 5 из 5)

mq::Queue qin, qout;qin.Init(pInfo->queue_in, MQ_RECEIVE_ACCESS);qout.Init(pInfo->queue_out, MQ_SEND_ACCESS);while(true){ const DWORD timeout = 500; BYTE* pBuffer = NULL; DWORD cbSize = 0; // получаем запрос от клиента HRESULT hr = qin.Receive(&pBuffer, &cbSize, timeout); if(SUCCEEDED(hr)) { CallHdr* pHdr = reinterpret_cast<CallHdr*>(pBuffer); cbSize -= sizeof(CallHdr); CComPtr<ICallUnmarshal> spUnmarshal; // создаем перехватчик на серверной стороне hr = CoGetInterceptor(pHdr->itf, 0, IID_ICallUnmarshal, (void**)&spUnmarshal); if(SUCCEEDED(hr)) { CComPtr<ICallFrame> spFrame; CALLFRAME_MARSHALCONTEXT ctx = { TRUE, MSHCTX_INPROC }; // выполняем преобразование буфера маршалинга в стек вызова hr = spUnmarshal->Unmarshal(pHdr->method, pBuffer + sizeof(CallHdr), cbSize, FALSE, pHdr->rep, &ctx, &cbSize, &spFrame); if(SUCCEEDED(hr)) { CComPtr<IUnknown> spUnk; // создаем экземпляр компонента hr = CoCreateInstance(pHdr->coclass, 0, CLSCTX_ALL, pHdr->itf, (void**)&spUnk); if(SUCCEEDED(hr)) { // вызываем исходный метод hr = spFrame->Invoke(spUnk.p); if(SUCCEEDED(hr)) { ctx.fIn = FALSE; cbSize = 0; free(pBuffer); pBuffer = NULL; // маршалинг out-параметров и результата HRESULT вызова hr = spFrame->GetMarshalSizeMax(&ctx, MSHLFLAGS_NORMAL, &cbSize); if(SUCCEEDED(hr)) { pBuffer = reinterpret_cast<BYTE*>(malloc(cbSize)); hr = spFrame->Marshal(&ctx, MSHLFLAGS_NORMAL, pBuffer, cbSize, &cbSize, 0, 0); } } } } } if(FAILED(hr)) { cbSize = sizeof(HRESULT); *reinterpret_cast<HRESULT*>(pBuffer) = htonl(hr); } // отправляем ответ клиенту hr = qout.Send(pBuffer, cbSize); if(pBuffer) { free(pBuffer); } } if(WaitForSingleObject(pInfo->hShutdown, timeout) == WAIT_OBJECT_0) break;}
ПРЕДУПРЕЖДЕНИЕВ методе ICallUnmarshal::Unmarshal явно указывается номер метода (первый параметр). Хотя в документации сказано, что возможное значение этого параметра -1 (в этом случае перехватчик сам определит номер метода, прочитав эту информацию из буфера с данными маршалинга), на практике такое значение использовать не удалось – при использовании -1 вызов ICallUnmarshal::Unmarshal завершается ошибкой доступа к памяти Access Violation в модуле ole32.dll

В рассмотренном выше примере нам не пришлось внести ни одного изменения ни в код клиента, ни в код компонента – благодаря технологии перехвата вызовов вся работа по организации взаимодействия клиента с компонентом происходит для них абсолютно прозрачно.

Использование такого “ручного” маршалинга параметров позволяет нам увидеть, какой информацией обмениваются proxy/stub в стандартной инфраструктуре COM. На иллюстрации приведен пример буфера, содержащего строчку BSTR, массив SAFEARRAY и объектную ссылку (указатель на интерфейс):

Рисунок 5. Буфер с in-параметрами вызова

Работа с параметрами вызова в обработчике

В примере выше мы делегировали работу по преобразованию стека вызова в бинарный буфер перехватчику. Бинарные буферы с параметрами вызова отлично подходят для многих видов транспорта – RPC, MSMQ. Однако если бы мы захотели использовать SOAP для передачи вызова компоненту, такое бинарное представление было бы неприемлемо, так как SOAP-сообщение представляет собой XML-текст, содержащий значения каждого из in-параметров по отдельности. Подробнее о протоколе SOAP и формате SOAP-сообщений можно прочитать в статье:

“Использование протокола SOAP в распределенных приложениях Microsoft SOAP Toolkit 3.0”.

В этой статье был рассмотрен способ создания Proxy, работающей через ранее связывание (SOAP Toolkit использует IDispatch и позднее связывание для вызовов). Proxy поддерживала интерфейс:

interface ISoapProxy : IDispatch{ [id(1), helpstring("method Initialize")] HRESULT Initialize([in]BSTR wsdl, [in]BSTR wsml, [in]BSTR service, [in]BSTR port); [propget, id(2), helpstring("property ConnectorProperty")] HRESULT ConnectorProperty([in]BSTR prop, [out, retval] VARIANT *pVal); [propput, id(2), helpstring("property ConnectorProperty")] HRESULT ConnectorProperty([in]BSTR prop, [in] VARIANT newVal); [propget, id(3), helpstring("property ProxyProperty")] HRESULT ProxyProperty([in]BSTR prop, [out, retval] VARIANT *pVal); [propput, id(3), helpstring("property ProxyProperty")] HRESULT ProxyProperty([in]BSTR prop, [in] VARIANT newVal); [id(4), helpstring("method GetOperation")] HRESULT GetOperation([in]BSTR name, [out,retval]IWSDLOperation** ppOp); [id(5), helpstring("method Execute")] HRESULT Execute([in]IWSDLOperation* pOp);};

Для каждого из интерфейсов/методов был написан код, перенаправляющий вызовы Proxy, которая, в свою очередь, использовала низкоуровневые компоненты из SOAP Toolkit для передачи вызова SOAP-серверу.

Например, реализация одного из методов выглядела так:

STDMETHOD(KillProcess)(LONG processID){ try { // получаемописаниеоперации KillProcess IWSDLOperationPtr spOp = m_spSoapProxy->GetOperation(L"KillProcess");IEnumSoapMappersPtr spEnum; // заполняем значения входных параметровspOp->GetOperationParts(&spEnum); while(true) { long l = 0; ISoapMapperPtr spMap; spEnum->Next(1, &spMap, &l); if(l == 1) { if(spMap->PartName == _bstr_t(L"processID")) spMap->ComValue = processID; } else break; } // передаемвызовсерверу m_spSoapProxy->Execute(spOp); } catch(_com_error & e) { return e.Error();} return S_OK;}
ПРИМЕЧАНИЕВ этом коде m_spSoapProxy – экземпляр Proxy (описание интерфейса см. выше)

Реализации для разных методов различались лишь названиями методов (или операций, в терминах SOAP) и названиями параметров. Вместо того, чтобы писать однотипный код, можно создать перехватчик для нужного интерфейса CoGetInterceptor, а в методе ICallFrameEvents::OnCall, напрямую манипулируя с параметрами вызова, создать SOAP-сообщение.

Получить значение параметра позволяет метод ICallFrame::GetParam:

HRESULT GetParam( ULONG iparam, VARIANT * pvar);

Нам нужен номер параметра, который можно получить из описания SOAP-операции ISoapMapper::get_CallIndex:

[propget] HRESULT callIndex([out, retval] long* par_lCallIndex);

После вызова метода нам потребуется метод для задания нового значения out-параметра в стеке ICallInfo::SetParam и метод для задания результата выполнения метода ICallInfo::SetReturnValue:

HRESULT SetParam( ULONG iparam, VARIANT * pvar);HRESULT SetReturnValue( HRESULT hr);

И, наконец, нужно отличать in- и out-параметры. Сделать это можно вызовом ISoapMapper::get_IsInput.

Полный код реализации обработчика вызова приведен ниже:

STDMETHOD(OnCall)(ICallFrame* pFrame){ LPWSTR lpszItf ,lpszMethod;HRESULT hr = pFrame->GetNames(&lpszItf, &lpszMethod); CoTaskMemFree(lpszItf); // получаем описание SOAP-операции из WSML CComPtr<IWSDLOperation> spOp; hr = m_spProxy->GetOperation(CComBSTR(lpszMethod) , &spOp); CoTaskMemFree(lpszMethod); if(SUCCEEDED(hr)) { CComPtr<IEnumSoapMappers> spEnum; hr = spOp->GetOperationParts(&spEnum); if(SUCCEEDED(hr)) { // перебираем все параметры while(hr == S_OK) { CComPtr<ISoapMapper> spMapper; long lFetched = 0; hr = spEnum->Next(1, &spMapper, &lFetched); if(!lFetched || hr != S_OK) break; long idx = 0; smIsInputEnum paramType; hr = spMapper->get_IsInput(&paramType); // для in-параметров берем значения из стека if(paramType == smInput || paramType == smInOut) { hr = spMapper->get_callIndex(&idx); if(SUCCEEDED(hr) && (idx >= 0)) { CComVariant value; hr = pFrame->GetParam(idx, &value); hr = spMapper->put_ComValue(value); } } } if(SUCCEEDED(hr)) { // выполняем вызов hr = m_spProxy->Execute(spOp); } } if(SUCCEEDED(hr)) { // перебираем все параметры spEnum->Reset(); while(hr == S_OK) { CComPtr<ISoapMapper> spMapper; long lFetched = 0; hr = spEnum->Next(1, &spMapper, &lFetched); if(!lFetched || hr != S_OK) break; smIsInputEnum paramType; hr = spMapper->get_IsInput(&paramType); // для out-параметров устанавливаем новое значение if(paramType == smOutput || paramType == smInOut) { long idx = 0; hr = spMapper->get_callIndex(&idx); if(SUCCEEDED(hr) && idx >= 0) { CComVariant value; hr = spMapper->get_ComValue(&value);hr = pFrame->SetParam(idx, &value); } } } } else { // если вызов завершился с ошибкой – устанавливаем return value pFrame->SetReturnValue(hr); } } return hr;}

Приведенный выше код работать не будет. :с))

Во-первых, вызов ISoapMapper::get_callIndex всегда возвращает -1, независимо от параметра.

Во-вторых, вызов ICallFrame::SetParam возвращает ошибку E_NOTIMPL, т.е. он попросту не реализован для перехватчика.

Обходной путь для первой проблемы заключается в использовании другого метода – IsoapMapper::get_ParameterOrder, возвращающего порядковый номер параметра в описании WSML. Как правило, порядковый номер в описании WSML соответствует порядковому номеру параметра в сигнатуре метода.

ПРИМЕЧАНИЕПо крайней мере, стандартный генератор WSML из SOAP Toolkit генерирует WSML именно так. Возможно, в будущих версиях SOAP Toolkit эта проблема будет исправлена, и мы сможем использовать более уместный в данном случае метод callIndex.

Решение второй проблемы не так очевидно. Необходимо каким-либо образом поместить в стек вызова значение out-параметра, но единственный подходящий для этих целей метод ICallFrame::SetParam возвращает E_NOTIMPL.

Разумеется, мы могли бы остановиться на этом. Наш пример корректно работает с in-параметрами, но не умеет передавать out-параметры.

Но все же есть способ добраться до местоположения адреса нужного параметра в стеке. Можно узнать адрес стека вызова с помощью ICallFrame::GetStackLocation:

PVOID GetStackLocation(void);

А также получить информацию о параметре метода, его местоположение в стеке:

typedef struct { BOOLEAN fIn; BOOLEAN fOut; ULONG stackOffset; ULONG cbParam;} CALLFRAMEPARAMINFO;HRESULT GetParamInfo( ULONG iparam,CALLFRAMEPARAMINFO * pInfo);

Теперь, если сложить адрес первого аргумента в стеке вызова и смещение нужного параметра CALLFRAMEPARAMINFO::stackOffset, мы получим адрес параметра в стеке. Код, заполняющий out-параметр выглядел так:

CComVariant value;hr = spMapper->get_ComValue(&value);hr = pFrame->SetParam(idx, &value);

Мы перепишем его так:

CComVariant value;hr = spMapper->get_ComValue(&value);PVOID pStack = pFrame->GetStackLocation();CALLFRAMEPARAMINFO info = {0};hr = pFrame->GetParamInfo(idx, &info);if(SUCCEEDED(hr) && info.cbParam == sizeof(long*)){ long** pParam = reinterpret_cast<long**>( reinterpret_cast<BYTE*>(pStack) + info.stackOffset);if(!IsBadReadPtr(*pParam, sizeof(long))) { VARIANT var = {}; value.Detach(&var); **pParam = var.lVal; }}
ПРИМЕЧАНИЕТакой способ не выглядит изящным, к тому же он не будет работать, если размер параметра в стеке отличается от 4.

Заключение

В первой части статьи мы познакомились с возможными способами перехвата вызовов методов COM-интерфейсов, их ограничения и слабые стороны, а также могли оценить сложность реализации универсального перехватчика.

Во второй части мы увидели, как наличие информации о сигнатурах методов может упростить реализацию перехватчика, и рассмотрели стандартную реализацию перехватчика из инфраструктуры COM/COM+ ICallInterceptor. Приведенные в статье примеры используют различные виды транспорта для передачи вызова от клиента компоненту – прямые вызовы, MSMQ, SOAP.

Список литературы

Алексей Остапенко. “Перехват методов интерфейса IUnknown”, RSDN Magazine, №3 2003.

Иван Андреев. “Использование протокола SOAP в распределенных приложениях Microsoft SOAP Toolkit 3.0”. RSDN Magazine №1 2003

MSDN, раздел Platform SDK: COM, описание CoGetInterceptor

Universal Delegator:

Building a Lightweight COM Interception Framework, Part 1: The Universal Delegator

Building a Lightweight COM Interception Framework, Part II: The Guts of the UD