developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Об интерфейсе возникновения денег

Говоря об интерфейсе IClassFactory невозможно пройти и мимо его проприетарного собрата - интерфейса IClassFactory2. Он предназначен в точности для того же, для чего предназначен и интерфейс IClassFactory, но только - для программ-серверов, в которые встроены какие-то средства ограничения работоспособности в зависимости от того "лицензированная" или "нелицензированная" копия программы запускается клиентом.

Вообще говоря, зная бизнес-модель компании Microsoft было бы удивительнее не обнаружить наличия такого интерфейса или какого-то подобного средства, чем обнаружить его, но, насколько этот интерфейс полезен "вне самой Microsoft"?

Я думаю, что - полезен. Не потому, что "противника нужно знать в лицо", а потому, что "лицензирование программ" - не блажь, а вполне необходимое занятие. И наличие стандартного на данной платформе интерфейса, который бы позволял единообразно различать "чистых" и "нечистых" является достоинством, а не недостатком - клиенты у COM-серверов могут быть разными, а точнее - круг клиентов заранее не ограничивается и даже не устанавливается. В этом - одна из самых сильных сторон COM. Но ведь и желание получить доход со своего продукта - тоже не последнее по значимости обстоятельство. Поэтому интерфейс IClassFactory2 является своего рода компромиссом между необходимостью обеспечить минимальную работоспособность сервера с любым клиентом (хотя бы для того, чтобы клиент не рухнул, а имел возможность корректно обработать специфическую ошибку) и нежеланием "дать попользоваться на халявку". Плохо это или хорошо c "идейной" стороны - пусть останется за рамками нашего рассмотрения, мы в данном случае изучаем только техническую возможность и как ею пользоваться. А когда и в каком объеме эту возможность применять - пусть разработчик сервера определяет сам.

Итак,вот список методов интерфейса IClassFactory2 (в порядке vtbl):

  1. QueryInterface
  2. AddRef
  3. Release
  4. CreateInstance
  5. LockServer

  6. GetLicInfo
  7. RequestLicKey
  8. CreateInstanceLic

Видно, что интерфейс IClassFactory2 является наследником интерфейса IClassFactory - его собственные методы только GetLicInfo, RequestLicKey и CreateInstanceLic.

Работает же вся эта механика так. Допустим, наш сервер работает в паре с каким-то механизмом, позволяющим по ходу исполнения программы выяснить - есть лицензия или нет. Это может быть "аппаратный ключ" (наподобие HASP), а может быть и какой иной механизм, важно, что внутри самой себя "защищённая программа" может выяснить есть "ключ" или нет. И в процессе своей инициализации она это обстоятельство выясняет. Алгоритм этого выяснения совершенно несущественен, важен только результат. Если "ключ - есть, ограничений - нет", то программа является полнофункциональной версией и совершенно свободно должна создавать экземпляры COM-типов посредством метода IClassFactory::CreateInstance... Если же это не так, т.е. "ключа - нет, ограничения - есть", то вызов IClassFactory::CreateInstance должен возвратить специализированный код ошибки CLASS_E_NOTLICENSED и не возвращать ссылку на экземпляр объекта.

Здесь должно быть понятно - поскольку IClassFactory обслуживает не весь сервер, а только один данный статический COM-тип, то и ограничения на запуск относятся не ко всему серверу в целом, а только к тем статическим типам, которые программист решил "предоставлять только в лицензированной версии". Поэтому вполне допустимо, если какие-то статические типы сервера будут "защищены ключом", а какие-то - будут доступны свободно. "Свободным типам" незачем реализовывать интерфейс IClassFactory2.

Программа-клиент, получив "отлуп" CLASS_E_NOTLICENSED, может обратиться к интерфейсу IClassFactory2 (который, в таком случае, статический COM-тип реализовывать должен) чтобы попытаться получить ссылку на экземпляр класса посредством метода IClassFactory2::CreateInstanceLic. Если же и это не получается, то клиент может утешиться хотя бы тем, что получит от IClassFactory2 "разъяснение своих прав". Т.е. сервер способен предоставить не только "двоичный" сервис "да/нет". Напротив, IClassFactory2 может снабдить сервер верхом обходительности с клиентом!

Делается всё это интерфейсом IClassFactory2 следующим образом. Под словами "ключ лицензии" понимается некоторая двоичная строка разумной длины, что-то наподобие "машинного пароля". И эта строка может играть роль "пароля на предъявителя". Предполагается, что если сервер выяснил, что он запускается в нелицензированной среде, то сервер всё равно "может быть столь любезен", чтобы дать клиенту возможность самому показать ему ключ лицензии. Этот "ключ" может не соотноситься с "ключом", который имеется у средств, которые контролируют "лицензированность" среды, т.е. этот "ключ" - "персональная отмычка" только для интерфейса IClassFactory2. Получив именно её IClassFactory2 должен "провернуться" так, как если бы он работал на лицензированной машине.

Для "выяснения своих прав" интерфейс IClassFactory2 предоставляет клиенту метод GetLicInfo:

HRESULT GetLicInfo(LICINFO * pLicInfo);

Клиент вызывает этот метод, предоставляя серверу пустую структуру LICINFO, а сервер её заполняет:

 typedef struct tagLICINFO{     ULONG cbLicInfo;     BOOL fRuntimeKeyAvail;     BOOL fLicVerified; } LICINFO;

В ней cbLicInfo - просто длина структуры, а значащими членами являются fRuntimeKeyAvail и fLicVerified. Оба принимают значение "==0" (false) и "!=0"(true). Параметр fLicVerified сообщает, каков ответ на вопрос "работает ли сервер в лицензированной среде?" и будут ли работать методы IClassFactory::CreateInstance и IClassFactory2::RequestLicKey. Если true - лицензия обнаружена.

Параметр fRuntimeKeyAvail сообщает, позволяет ли сервер делегировать право создания экземпляров данного статического типа нелицензированной машине. Если true - позволяет, т.е. можно бежать к лицензированной машине, вызывать метод IClassFactory2::RequestLicKey на ней, получать ключ лицензии, тащить его к себе и вызывать у своего сервера метод IClassFactory2::CreateInstanceLic с этим самым ключом. Если возвращается значение false, то делать это бесполезно - метод IClassFactory2::CreateInstanceLic всё равно откажется работать :(

Если интерфейс IClassFactory2 реализован, то как минимум, метод GetLicInfo у него должен работать - именно этот метод и сообщает клиенту работоспособны ли два других метода. И ещё важное замечание - совсем не так, как это принято в большинстве COM-методов, пустую структуру для заполнения в этом методе должен предоставить клиент, а не сервер. Должен ли клиент перед вызовом этого метода заполнить поле длины структуры? Не знаю... Реализация этого метода - ваша, собственная. Вы можете проверять эту длину и, если она коротка - отказаться заполнять структуру. А можете и не обращать на это внимания, всегда предполагая, что длина - достаточна. В реализации этого метода в ATL сервер ничего не проверяет, кроме того не равен ли указатель, который он получил от клиента, NULL и помещает в поле длины чрезвычайно "содержательную" информацию - sizeof(LICINFO).

Следующий метод - IClassFactory2::RequestLicKey снабжает клиента пролицензированной машины (на нелицензированной машине метод должен отказаться работать) ключом лицензии (видимо, для того, чтобы тот мог "поделиться с друзьями"):

HRESULT RequestLicKey(     DWORD dwReserved, //Не используется, должен быть 0     BSTR * pbstrKey   //Указатель на ключ лицензии );

Здесь pbstrKey - предоставляемый клиентом указатель, на строку типа BSTR, куда сервер поместит адрес размещенного "ключа лицензии". Но в этом методе, как и положено в COM, память под собственно строку ключа - предоставляет сервер. И её после этого - надо вернуть системе. Поскольку реализация - ваша, то размещать эту память вы будете функцией SysAllocString, а клиент будет освобождать вызывая функцию SysFreeString. Но об именно размещении/освобождении строк - в нашем же изложении, только позже. Полученный ключ клиент может использовать в вызове метода IClassFactory2::CreateInstanceLic.

Нужно отметить, что RequestLicKey отработает успешно только в том случае, если это "не запрещено творцом" реализации данного COM-класса, т.е. если метод GetLicInfo возвращает член LICINFO::fRuntimeKeyAvail со значением true. Если возвращается LICINFO::fRuntimeKeyAvail == false, то и метод RequestLicKey должен вернуть код E_NOTIMPL - метод не реализован.

Ну и, наконец, предел всем мытарствам с обретением ключа лицензии на запуск и созданием экземпляра кладёт метод CreateInstanceLic:

HRESULT CreateInstanceLic(     IUnknown * pUnkOuter,    //IUnknown агрегата     IUnknown * pUnkReserved, //Не используется. Должен быть NULL     REFIID riid,             //IID запрашиваемого интерфейса     BSTR bstrKey,            //ключ лицензии     void **ppvObject         //адрес указателя под ссылку );

Метод очень похож на метод CreateInstance, только появляются параметры pUnkReserved и bstrKey.

При этом отметьте пожалуйста - CreateInstanceLic тоже "условно-работоспособный" метод. Он может принять bstrKey и создать экземпляр объекта на нелицензированной машине как на лицензированной. А может и не принять и не создать - это опять определяет творец реализации данного COM-класса. Если методу RequestLicKey запрещено возвращать ключ лицензии, то и метод CreateInstanceLic должен всегда возвращать код возврата E_NOTIMPL - создание объектов данного типа разрешено только на полностью пролицензированной машине. А делает это IClassFactory::CreateInstance.

Поэтому неразобранным остаётся только один вопрос - а как применять-то? Да вот хотя бы и так - вы выпускаете в обращение "сервер с ключом". Понятно, что появляются его нелицензионные копии... Но вам это даже и на руку, т.к. "номальный" клиент, который делает всё, использует для создания экземпляров метод IClassFactory::CreateInstance и который "без ключа" не работает, а в распространяемого вами "демонстрационного" клиента "забит" ключ лицензии на предъявителя и использует этот клиент IClassFactory2::CreateInstanceLic. И с этим клиентом работает всё - но в демонстрационном варианте... А сервер - один и тот же.

С методической точки зрения интерфейс IClassFactory2 интересен с нескольких сторон. Во-первых, это живая иллюстрация принципа - опубликованный интерфейс никогда не изменяется. Если вам требуется модернизировать существующий интерфейс, то нельзя делать его версию. Нужно разработать совершенно новый (с другим IID) интерфейс и опубликовать его. Что мы и видим, IID_IClassFactory - {00000001-0000-0000-C000-000000000046}, а IID_IClassFactory2 - {B196B28F-BAB4-101A-B69C-00AA00341D07}. Во-вторых, это иллюстрация, как именно можно и нужно расширять функциональность существующих (опубликованных, известных) интерфейсов на какой-то частный случай применения. Мы будем впоследствии рассматривать этот вопрос отдельно и подробно, но в COM действует принцип "лучше реализовывать много небольших интерфейсов, чем несколько - больших". В-третьих, это пример протокола, когда два разных интерфейса (пусть они в данном случае и могут быть реализованы в одной vtbl, но так бывает не всегда) работают в паре - если IClassFactory "проваливается", то клиенту должен предлагаться другой интерфейс. А оба они, скорее всего, реализованы внутри сервера так тесно, что их и не разделить. В-четвертых... я понимаю, что это очень "дискуссионабельный" вопрос - IClassFactory2 есть ещё и пример как соединить байты и центы и при этом остаться вежливым к клиенту с самым разным "уровнем доходов". Мы часто наблюдаем, что программа, в которую встроены всякого рода "защиты по ключу" то просто виснет, то вообще не даёт на себя посмотреть без этого самого ключа. А ведь можно - и чуть-чуть по другому...

Ну и уж коль скоро мы затронули эту тему, нужно заметить, что вообще COM в смысле сохранения проприетарности сконструирован просто замечательно. Если вы рассматриваете DLL, то как минимум, вседа сможете увидеть её таблицы импортируемых и экспортируемых функций и глядя на них вычислить точки входа и как-то соотнести мнемонику имён с функциями, которые они выполняют. А вот рассматривая COM-сервер... вы не выясните ничего. Вы не сможете выяснить у самого сервера (библиотеки типов не касаемся!) ни какие интерфейсы он поддерживает, ни состав этих интерфейсов, ни точки входа... Даже методом перебора это вряд-ли удастся установить в силу общего числа возможных GUID. Эту информацию приходится публиковать в системном реестре, если у вас local или remote сервер - без неё не работает системный слой поддержки COM, а вот для inproc-сервера её публикация не обязательна - наш сервер из примера работал с тем минимумом опубликованной информации, чтобы система могла его отыскать для загрузки на исполнение. Поэтому, если в вашем проекте есть некая особенность, которую бы вы хотели подальше спрятать от любопытных глаз исследователя с отладчиком, так, чтобы он и догадаться-то не мог о том, что она есть и когда-то откуда-то вызывается - оформите её inproc-сервером, который замаскируйте под обычную DLL. Ничто не запрещает, чтобы COM-сервер был бы "COM-сервером" только наполовину и экспортировал бы какие-то другие функции как обычная DLL. Но можно сделать и ещё хитрее - и функцию DllGetClassObject переименовать, т.е. на принципах, на каких построен COM-сервер, сделать свою реализацию "внутреннего маленького COM" протокол связи с которым был бы известен только в рамках данного проекта.

На этом всё. В следующей статье мы будем рисовать картинки обо всём изученном о конструкции COM-сервера.

предыдущий выпускархив и оглавлениеследующий выпуск
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

разделы сайта

форум

технология COM

оптимизация сайтов


© 2000-2004 Клуб программистов developing.ru