Programowanie rozproszone - DCOM
Technologia COM, której zarys i przykład zastosowania przedstawiliśmy w poprzednim rozdziale, zdobywa sobie coraz większą popularność. Coraz więcej systemowych usług Windows implementowanych jest już nie w postaci tradycyjnych interfejsów programisty (API - Application Programming Interface), lecz właśnie w formie obiektów COM. Mimo to jest ona - w swej oryginalnej postaci - dotknięta przez jedną poważną przypadłość, jaką jest ograniczenie obszaru działania do granic pojedynczego komputera: nie jest mianowicie możliwe umieszczenie klienta na komputerze innym niż serwer - dzieje się tak mimo „przezroczystego” z założenia charakteru samej technologii, to jest niewrażliwości zarówno klienta, jak i serwera na fizyczną lokalizację swego partnera.
Receptą na opisaną dolegliwość okazało się rozszerzenie technologii COM o elementy sieciowe, umożliwiające aktywowanie przez klienta zdalnych serwerów, rezydujących na innych komputerach. Owa rozszerzona postać COM, zwana rozproszonym COM i oznaczana akronimem DCOM (ang. Distributed COM), oprócz elementów komunikacji sieciowej pomiędzy komponentami wprowadza ponadto dodatkowe elementy, głównie o charakterze zabezpieczającym. Przyjrzymy się im dokładniej w tym rozdziale.
Czym jest DCOM?
Specyfikacja DCOM to nic innego jak sieciowa wersja COM, określana niekiedy żargonowo jako „COM na drutach” (ang. wired COM). Obiekt COM osiągalny nie bezpośrednio, lecz poprzez sieć, jest obiektem DCOM. Technicznie rzecz biorąc, DCOM stanowi obiektowe rozszerzenie protokołu zdalnego wywoływania procedur (ang. RPC - Remote Procedure Calls), stworzone wraz z pojawieniem się Windows NT 4.0. Popularność tych ostatnich w zastosowaniach korporacyjnych sprawiła, iż mimo innych konkurencyjnych technologii DCOM wciąż zajmuje wiodącą pozycję; technologia ta przekroczyła już zresztą ramy samych Windows, znajdując zastosowanie w kilku wersjach UNIX-a i jako taka może być obecnie używana jako międzyplatformowe (cross-platform) rozwiązanie projektowe dla systemów rozproszonych.
DCOM wykorzystuje oryginalną koncepcję COM tzw. przezroczystej lokalizacji (ang. location transparency), co oznacza, iż wzajemne położenie fizyczne klienta i serwera jest nieistotne - mogą one rezydować na tym samym komputerze, na dwóch różnych komputerach w sieci lokalnej czy nawet - dzięki Internetowi - na różnych kontynentach. Z punktu widzenia klienta serwer DCOM zlokalizowany jest w miejscu określonym przez stosowny wpis w Rejestrze systemowym.
Ta prostota jest jednakże tylko jedną stroną medalu. Zdalny dostęp do serwera obwarowany jest wieloma regułami bezpieczeństwa, wynikającymi z tzw. sieciowego modelu domenowego (ang. Network Domain Model) - uzyskanie dostępu do obiektu COM serwera DCOM związane jest nierozerwalnie z logowaniem się klienta i weryfikacją jego tożsamości oraz uprawnień. Elementy składające się na system zabezpieczeń mogą być przy tym określane statycznie, za pomocą wpisów w Rejestrze - mamy wówczas do czynienia z bezpieczeństwem deklaratywnym - albo dynamicznie w kodzie aplikacji - co nazywane bywa bezpieczeństwem programowanym.
W tym rozdziale zobaczymy, jak C++Builder realizuje ideę bezpieczeństwa programowanego, przedstawimy też oczywiście sposoby aktywowania zdalnego obiektu DCOM.
Rodzina systemów Windows a DCOM
Technologia DCOM wbudowana jest organicznie w Windows 98, Windows NT 4.0 i Windows 2000; Windows 95 wymagają w tym celu zainstalowania dodatkowego składnika o nazwie DCOM95, który można ściągnąć z firmowej witryny Microsoftu (http://www.microsoft.com/com) i który instalowany jest także wraz z Internet Explorerem w wersji 4. lub 5. (przy pełnej instalacji).
Jeżeli Twoja sieć wyposażona jest w kontroler domen (Domain Controller), powinieneś skonfigurować w swych komputerach zabezpieczenia Windows 95/98 jako zorientowane na użytkowników (user-level security); w przeciwnym razie nie będzie możliwe przeprowadzanie uwierzytelnień wywołań DCOM.
Z punktu widzenia bezpieczeństwa sieci Windows 95/98 bardzo dobrze nadają się do implementacji klientów DCOM - nie mają bowiem wbudowanych rodzimych mechanizmów zabezpieczających oraz nie mogą uruchamiać serwerów DCOM w sposób dynamiczny.
Konfigurowanie DCOM - program DCOMCnfg
Program DCOMCnfg.exe (DCOM Configuration Utility) jest narzędziem administratora, umożliwiającym modyfikację ustawień DCOM zarówno w stosunku do pojedynczego serwera, jak i wszystkich zarejestrowanych serwerów DCOM, znajdujących się na danym komputerze. Okno główne programu (rys. 13.1) składa się z czterech następujących kart: Aplikacje, Domyślne właściwości, Domyślne zabezpieczenia i Protokoły domyślne.
Niniejsza dyskusja na temat programu DCOMCnfg.exe odnosi się do wersji znajdującej się w Windows NT 4.0 z poprawką nr 4 (Service Pack 4) lub późniejszą oraz w Windows 2000. Windows 9x posługują się nieco zmodyfikowaną wersją tego narzędzia.
Rysunek 13.1. Okno główne programu konfiguracyjnego DCOM (DCOMCnfg.exe)
Ustawienia ogólnosystemowe
Karta Aplikacje okna z rys. 13.1 zawiera wykaz wszystkich aplikacji, z którymi związane są zarejestrowane serwery DCOM istniejące na danym komputerze. Po wybraniu określonego serwera i kliknięciu przycisku Właściwości uzyskujemy dostęp do jego ustawień.
Za pośrednictwem karty Domyślne właściwości (rys. 13.2) możliwe jest m.in. udostępnienie albo zablokowanie mechanizmów DCOM na danym komputerze, a także określenie domyślnych dla danego komputera ustawień uwierzytelnień i uosobienia.
Jedna z opcji tej karty określa dostępność internetowych usług COM (CIS - COM Internet Services) na danym komputerze. Usługi te wprowadzają do modelu DCOM nowe elementy, jak np. komunikację za pośrednictwem serwerów proxy i firewalli. Więcej informacji na temat usług CIS znajduje się na firmowej stronie Microsoftu pod adresem http://msdn.microsoft.com/library/backgrnd/html/cis/htm.
Rysunek 13.2. Domyślne ustawienia DCOM na określonym komputerze
Uwierzytelnienie może być określone na jednym z trzech następujących poziomów:
dla pierwszego połączenia klienta;
dla każdego wywołania metody serwera;
dla każdego przesłania pakietu pomiędzy klientem a serwerem.
Można także całkowicie wyłączyć uwierzytelnianie, co jakkolwiek jest nieco ryzykowne, to jednak mogą na tym skorzystać niektórzy użytkownicy, szczególnie ci łączący się z serwerem za pośrednictwem Internetu.
Domyślny poziom uosobienia to poziom uprawnień, jakie przyznawane są aplikacji serwera przez aplikację klienta do wykonywania określonych działań w imieniu tej ostatniej. Poziom tych uprawnień może przyjmować następujące wartości, w kolejności od najniższego do najwyższego zabezpieczenia:
Anonimowy - tożsamość aplikacji klienta nie jest znana serwerowi;
Identyfikacja - aplikacja serwera może sprawdzić tożsamość aplikacji klienta;
Uosobienie - aplikacja serwera może personifikować aplikację klienta dla dostępu w jej imieniu do lokalnych zasobów komputera;
Delegat - aplikacja serwera może wykonywać określone działania na innych komputerach jako aplikacja klienta. Ten wariant uosobienia dostępny jest tylko w Windows 2000 z uruchomionym systemem Kerberos.
Na karcie Domyślne zabezpieczenia (rys. 13.3) możliwe jest określenie domyślnych uprawnień dostępu do aplikacji, uruchamiania aplikacji i konfiguracji OLE.
Rysunek 13.3. Ustawienia domyślnych zabezpieczeń DCOM
Domyślne uprawnienia dostępu określają, kto ma prawo dostępu do serwerów DCOM zarejestrowanych na komputerze. Wartość Wszyscy (Everyone) oznacza dostęp dla każdego użytkownika.
Domyślne uprawnienia uruchamiania dotyczą praw do uruchamiania procesów serwera; wartość Wszyscy (Everyone) oznacza dowolnego użytkownika. Ta opcja nie może być ustawiana w sposób programowy.
Domyślne uprawnienia konfigurowania związane są z instalacją nowych serwerów OLE i zmianami w serwerach istniejących i określają rodzaj dostępu (Czytaj, Pełny dostęp, Specjalny dostęp) dla poszczególnych użytkowników.
Karta Protokoły domyślne (rys. 13.4) wykorzystywana jest do ustalenia dostępności i kolejności dostępu poszczególnych protokołów sieciowych używanych przez DCOM. Klikając przycisk Właściwości, można ustalić pewne aspekty funkcjonowania protokołu, na przykład dostęp za pośrednictwem firewalla. Generalnie ustawienia domyślnych protokołów nie powinny być zmieniane przez niezaawansowanych użytkowników.
Rysunek 13.4. Karta domyślnych protokołów DCOM
Nie należy zapominać, iż ustawienia dokonywane na kartach Domyślne właściwości i Domyślne zabezpieczenia są ustawieniami ogólnosystemowymi - ich zmiana może spowodować komplikację w pracy niektórych serwerów funkcjonujących dotąd bez zarzutu. O wiele bezpieczniejsze są ustawienia dokonywane w odniesieniu do konkretnego serwera.
Ustawienia związane z konkretnym serwerem
Zmianę ustawień określonego serwera rozpoczynamy od wybrania go z listy przedstawionej na rysunku 13.1 i kliknięciu przycisku Właściwości. Wyświetlone w wyniku tego okno dialogowe właściwości serwera podzielone jest na pięć kart (rys. 13.5).
Tu proszę wkleić rysunek z pliku orig-17-05.bmp
Rysunek 13.5. Właściwości serwera DCOM
Na karcie Ogólne (rys. 13.5) widzimy zarejestrowaną nazwę serwera, jego typ, fizyczną lokalizację oraz poziom uwierzytelnień - ten ostatni może być zmieniany bez wpływu na inne serwery.
Karta Lokalizacja (rys. 13.6) umożliwia wybór komputera, na którym serwer będzie uruchamiany - mamy do dyspozycji komputer zawierający bazę danych, komputer, na którym uruchomiono program DCOMCnfg, oraz komputer o wskazanej nazwie.
Tu proszę wkleić rysunek z pliku orig-17-06.bmp
Rysunek 13.6. Lokalizacja punktu uruchomienia serwera
Karta Zabezpieczenia (rys. 13.7) jest odpowiednikiem karty zabezpieczeń domyślnych w ustawieniach ogólnosystemowych i dla każdej kategorii uprawnień (dostęp, uruchamianie, konfiguracja) umożliwia dokonanie ustawień specyficznych dla serwera bądź skorzystanie z domyślnych ustawień systemu.
Tu proszę wkleić rysunek z pliku orig-17-07.bmp
Rysunek 13.7. Ustawienia zabezpieczeń serwera
Karta Identyfikacja (rys. 13.8) służy do określenia użytkownika, w którego kontekście zabezpieczenia uruchomiona zostanie aplikacja serwera. Mamy tu do wyboru trzy opcje:
Użytkownik aktywny - oznacza użytkownika zalogowanego do komputera („interaktywnego”); jeżeli do komputera nie jest zalogowany żaden użytkownik, próba uruchomienia aplikacji serwera zakończy się niepowodzeniem;
Użytkownik wywołujący - oznacza użytkownika żądającego dostępu do obiektu serwera; dla każdego klienta tworzona będzie oddzielna kopia procesu serwera. Jest to ustawienie domyślne, nie jest jednak najszczęśliwsze dla aplikacji rozproszonej i jako takie powinno być unikane;
Ten użytkownik - oznacza specyficznego użytkownika; może określać konto już zdefiniowane lub utworzone ad hoc. W większości przypadków ta opcja jest najodpowiedniejsza, dzięki niej można bowiem z góry określić zabezpieczenia uruchamianej aplikacji.
Dla każdego serwera, który nie został skonfigurowany za pomocą programu DCOMCnfg.exe, DCOM używa domyślnych ustawień systemowych zapamiętanych w Rejestrze. Specyficzne ustawienia serwera mają pierwszeństwo przed ustawieniami ogólnosystemowymi, zaś aktualne ustawienia serwera - niezależnie od tego, czy dziedziczone z ustawień ogólnosystemowych, czy też specyficzne - mogą być (z nielicznymi wyjątkami) zmieniane w sposób programowy.
Należy zawsze pamiętać o ustawieniu uprawnień uruchamiania serwera - zaniedbanie tej czynności może sprawić, iż serwer pozostanie niedostępny dla zdalnych użytkowników.
Tu proszę wkleić rysunek z pliku orig-17-08.bmp
Rysunek 13.8. Identyfikacja użytkownika uruchamiającego aplikację serwera
Nie będziemy się tu zajmować kartą Punkty końcowe - zainteresowanych Czytelników odsyłamy do systemu pomocy programu DCOMCnfg.
Zastosowanie DCOM - przykład
Prezentację mechanizmów DCOM przeprowadzimy na podstawie dwóch projektów, zlokalizowanych w podkatalogach Server i Client katalogu głównego o nazwie EasyDCOM. Kompletny kod źródłowy obydwu projektów znajduje się na załączonej do książki płycie CD-ROM.
Tworzenie aplikacji serwera
Rozpoczniemy od projektu serwera. W tym celu:
Zainicjuj nową aplikację (File|New Application).
Przejdź do folderu EasyDCOM\Server i zapisz projekt pod nazwą EasyDCOM.bpr, a jego moduł główny pod nazwą MainUnit.cpp.
Za pomocą inspektora obiektów ustaw nazwę formularza na frmMain, a następnie umieść na nim stosowną etykietę i zmień jego rozmiary tak, by wyglądał podobnie do rysunku 13.9
Tu proszę wkleić rysunek z pliku orig-17-09.bmp
Rysunek 13.9. Formularz główny serwera
Wyświetl okno New Items (File|New), przejdź na jego kartę ActiveX i kliknij dwukrotnie pozycję Automation Object.
Zostanie wyświetlone okno dialogowe New Automation Object. Wpisz HostInfo jako nazwę koklasy i „Easy DCOM Library 1.0” w pole Description. Kliknij przycisk OK.
Za pomocą opcji View|Type Library uczyń okno edytora TLE oknem pierwszoplanowym. W jego lewym panelu podświetl pozycję reprezentującą interfejs IHostInfo.
Rozwiń listę stowarzyszoną z przyciskiem New Property na pasku narzędziowym (drugi od lewej w prawej sekcji przycisków) i wybierz z niej opcję Read Only.
Zmień domyślną nazwę nowej właściwości (Property1) na Info.
Na prawym panelu przejdź do karty Parameters i zmień typ parametru na BSTR* (patrz rys. 13.10)
Tu proszę wkleić rysunek z pliku orig-17-10.bmp
Rysunek 13.10. Właściwość Info w oknie edytora TLE
Kliknij przycisk Refresh na pasku narzędziowym w celu odświeżenia zawartości plików źródłowych.
Przejdź do pliku HostInfoImpl.cpp i dodaj następujący kod do metody get_Info():
STDMETHODIMP THostInfoImpl::get_Info(BSTR* Value)
{
try {
*Value = NULL;
char lpBuffer[MAX_COMPUTERNAME_LENGTH + 1];
unsigned long lngSize = sizeof(lpBuffer);
if (GetComputerName(lpBuffer, &lngSize) == 0)
return HRESULT_FROM_WIN32(GetLastError());
WideString strInfo = AnsiString().sprintf("Date and time at %s is:
%s", lpBuffer, DateTimeToStr(Now()));
*Value = strInfo.Detach();
}
catch(Exception &e) {
return Error(e.Message.c_str(), IID_IHostInfo);
}
return S_OK;
};
Skompiluj i uruchom serwer (naciskając F9).
Zatrzymajmy się przez chwilę przy funkcji THostInfoImpl::get_Info(). Rozpoczyna się ona wywołaniem funkcji Win32 API GetComputerName(), wpisującej nazwę komputera do zmiennej lpBuffer. Nazwa ta, po uzupełnieniu jej o bieżącą datę i czas, wpisywana jest do pomocniczego łańcucha strInfo. Zawartość tego łańcucha zostaje następnie przepisana do parametru Value i jednocześnie od niego „odczepiona”, by po zakończeniu funkcji nie została zwolniona zajmowana przez nią pamięć - jak pamiętamy z rozdziału 12., parametry wyjściowe (out) alokowane są przez serwer i zwalniane przez klienta.
Tworzenie aplikacji klienta
Scenariusz tworzenia naszej przykładowej aplikacji - klienta DCOM jest następujący:
Zainicjuj nową aplikację i zapisz jej pliki w katalogu EasyDCOM\Client; modułowi głównemu nadaj nazwę MainUnit.cpp, a plikowi projektu - EasyDCOMClient.bpr.
Umieść na formularzu głównym wymienione poniżej komponenty i zmień ich nazwy, jak w tabeli:
Nazwa oryginalna |
Nowa nazwa |
Form1 |
frmMain |
GroupBox1 |
gbxLocal |
GroupBox2 |
gbxRemote |
Edit1 |
txtLocal |
Edit2 |
txtRemote |
Button1 |
cmdInfo |
Button2 |
cmdFinish |
Ustaw właściwości komponentów tak, by nadać formularzowi wygląd pokazany na rysunku 13.11.
Rysunek 13.11. Formularz główny aplikacji - klienta
Otwórz plik EasyDCOMClient.cpp i dodaj do niego następującą dyrektywę:
USEUNIT("..\Server\EasyDCOM_TLB.cpp");
Otwórz plik MainUnit.h i dodaj do niego następującą dyrektywę:
#include "..\Server\EasyDCOM_TLB.h"
Dodaj następującą deklarację w prywatnej sekcji formularza głównego TfrmMain:
TCOMIHostInfo m_objHost;
Kliknij dwukrotnie formularz główny i dodaj następujący kod do funkcji obsługującej jego zdarzenie OnCreate:
void __fastcall TfrmMain::FrmMain_Create(TObject *Sender)
{
char szBuffer[MAX_COMPUTERNAME_LENGTH + 1];
unsigned long lngSize = sizeof(szBuffer);
GetComputerName(szBuffer, &lngSize);
txtLocal->Text = AnsiString().sprintf("Date and time at %s is: %s",
szBuffer, DateTimeToStr(Now()));
}
Kliknij dwukrotnie przycisk cmdInfo i dodaj następujący kod do jego funkcji zdarzeniowej:
void __fastcall TfrmMain::CmdInfo_Click(TObject *Sender)
{
if (!m_objHost.IsBound())
{
AnsiString strHost;
if (InputQuery("Create Server", "Enter computer name:", strHost))
{
if (strHost.IsEmpty())
OleCheck(CoHostInfo::Create(m_objHost));
else
OleCheck(CoHostInfo::CreateRemote(WideString(strHost), m_objHost));
WideString strInfo;
OleCheck(m_objHost.get_Info(&strInfo));
txtRemote->Text = strInfo;
}
}
}
Kliknij dwukrotnie przycisk cmdFinish i dodaj do jego funkcji zdarzeniowej pojedynczą instrukcję:
Close();
Za pomocą inspektora obiektów utwórz funkcję obsługi zdarzenia OnClose formularza głównego i wpisz do jej wnętrza następującą instrukcję:
m_objHost.Unbind();
Skompiluj i uruchom aplikację klienta.
Przeanalizujmy pokrótce przedstawiony kod.
Funkcja obsługująca zdarzenie OnCreate formularza pobiera i wyświetla nazwę lokalnego komputera oraz bieżącą datę i czas.
Po kliknięciu przycisku cmdInfo funkcja obsługująca to zdarzenie wywołuje metodę IsBound() „eleganckiego” interfejsu TCOMIHostInfo w celu stwierdzenia, czy obiekt serwera został już utworzony - jeżeli nie, to funkcja zdarzeniowa kończy swą pracę.
Funkcja InputQuery() pobiera nazwę NETBIOS, nazwę DNS i adres IP zdalnego komputera i zapisuje te dane w zmiennej strHost.
Jeżeli łańcuch strHost jest pusty, następuje lokalne utworzenie serwera za pomocą metody Create(), w przeciwnym razie metoda CreateRemote() próbuje utworzyć obiekt serwera na wskazanym zdalnym komputerze. Poprawność wywołań obydwu metod kontrolowana jest przez funkcję OleCheck().
Wywoływana jest metoda get_Info() z adresem łańcucha WideString jako jedynym parametrem. Łańcuch ten jest następnie wypełniany informacjami uzyskanymi ze zdalnego komputera, po czym informacje te wyświetlane są w ramach kontrolki txtRemote.
Przed zamknięciem formularza wywoływana jest metoda Unbind() „eleganckiego” interfejsu TCOMIHostInfo, dokonująca zwolnienia obiektu serwera.
Pora teraz na zasadniczy test, czyli przeniesienie aplikacji serwera (EasyDCOM.exe) na inny komputer, jednokrotne jej uruchomienie w celu autorejestracji, a następnie próbę uzyskania dostępu do niej przez aplikację klienta.
Jeżeli serwer znajduje się na komputerze z systemem Windows 9x, nie nastąpi jego automatyczne uruchomienie - trzeba go uruchomić w sposób jawny.
Konfigurowanie uprawnień dostępu i uruchamiania serwera
Załóżmy na potrzeby naszego przykładu, iż komputer klienta i zdalny komputer serwera rezydują w domenie sieciowej Microsoft Network i dokonajmy ustawienia specyficznych dla serwera uprawnień uruchamiania i dostępu.
W tym celu należy uruchomić program DCOMCnfg i z karty Aplikacje wybrać pozycję reprezentującą nasz serwer - „Easy DCOM Type Library 1.0”. Po dwukrotnym kliknięciu tej pozycji wyświetlone zostanie okno dialogowe właściwości serwera; należy wówczas przejść na jego kartę Zabezpieczenia, zaznaczyć opcję „Użyj niestandardowych uprawnień uruchamiania” i kliknąć przycisk „Edytuj” związany z tą opcją. Wyświetlone zostanie okno rejestracji uprawnień (rys. 13.12); za pomocą przycisku „Dodaj” należy dodać do tego okna konto „Wszyscy”. W ten sposób zezwalamy wszystkim użytkownikom na uruchamianie serwera.
Tu proszę wkleić rysunek z pliku orig-17-12.bmp, po uprzednim usunięciu niepotrzebnych elementów na drugim planie (tak, aby zostały tylko dwa interesujące nas okna, podobnie jak w oryginale na stronie 730).
Rysunek 13.12. Zezwolenie wszystkim użytkownikom na uruchamianie serwera
Dostęp do usług uruchomionego serwera określony jest obecnie przez ustawienia ogólnosystemowe. Możemy zmienić ten stan rzeczy, zaznaczając opcję „Użyj niestandardowych uprawnień dostępu” i po kliknięciu związanego z nią przycisku Edycja wybrać użytkowników, którym zezwala się na dostęp (rys. 13.13).
Tu proszę wkleić rysunek z pliku orig-17-13.bmp, po uprzednim usunięciu niepotrzebnych elementów na drugim planie (tak, aby zostały tylko dwa interesujące nas okna, podobnie jak w oryginale na stronie 731).
Rysunek 13.13. Specyfikacja użytkowników posiadających dostęp do usług serwera
Konfigurowanie identyfikacji
Jak już wcześniej pisaliśmy, za pomocą karty Identyfikacja właściwości serwera (patrz rysunek 13.8) możemy określić, w kontekście zabezpieczeń którego użytkownika uruchamiana będzie aplikacja serwera. Najbardziej „konkretną” z trzech możliwych opcji jest oczywiście określenie konkretnego użytkownika - należy podać jego nazwę i hasło (dwukrotnie), a następnie kliknąć OK.
Uruchomienie przykładu
Określiliśmy już zarówno uprawnienia uruchamiania serwera i dostępu do niego, jak również użytkownika, w kontekście którego uruchamianie to będzie następować. Pora więc wypróbować funkcjonowanie serwera.
Uruchom aplikację klienta na komputerze lokalnym, kliknij przycisk Go Get i w wyświetlone okno dialogowe wprowadź nazwę zdalnego komputera lub jego adres IP. Jeżeli wszystko przebiegnie pomyślnie, powinieneś zobaczyć nazwę zdalnego komputera i wskazanie jego zegara, jak pokazuje to rysunek 13.14.
Tu proszę wkleić rysunek z pliku orig-17-14.bmp
Rysunek 13.14. Dialog aplikacji - klienta z serwerem na zdalnym komputerze
Bezpieczeństwo programowane
Ustawienia związane z poziomem zabezpieczeń, dokonywane przez program DCOMCnfg, odczytywane są (z Rejestru) przez aplikację COM i przekazywane do funkcji API o nazwie CoInitializeSecurity(). Funkcja ta wywoływana jest dokładnie raz dla każdego procesu wykorzystującego obiekt COM, dzięki czemu ustalone zostają dla tego procesu elementy składające się na jego bezpieczeństwo - poziom uwierzytelnień, lista uprawnionych użytkowników, postać pakietów danych związanych z zabezpieczeniami itp. W dalszej części rozdziału pokażemy, jak samodzielnie posługiwać się wspomnianą funkcją.
Parametry funkcji CoInitializeSecurity
Funkcja CoInitializeSecurity() jest jedną z najważniejszych funkcji COM, związanych z jego bezpieczeństwem. Posługuje się ona następującymi parametrami wywołania:
IN PSECURITY_DESCRIPTOR pSecDesc // wskaźnik na deskryptor
// zabezpieczenia
IN LONG cAuthSvc // liczba pozycji w asAuthSvc
IN SOLE_AUTHENTICATION_SERVICE *asAuthSvc // tablica rejestrowanych nazw
IN void *pReserved1 // zarezerwowane dla przyszłego
// użytku
IN DWORD dwAuthnLevel // domyślny poziom
// uwierzytelnienia
IN DWORD dwImpLevel // domyślny poziom uosobienia
IN SOLE_AUTHENTICATION_LIST *pAuthList, // informacja o
// uwierzytelnieniach dla każdej
// usługi uwierzytelniającej
IN DWORD dwCapabilities // dodatkowe możliwości klienta
// i (lub) serwera
IN void *pReserved3 // zarezerwowane dla przyszłego
// użytku
Niektóre z wymienionych parametrów odnoszą się zarówno do klienta, jak i do serwera, niektóre tylko do jednego z nich.
Pierwszy z parametrów - pSecDesc - istotny jest tylko wówczas, gdy proces jest procesem serwera. Jego zawartością jest wówczas wskaźnik na: GUID aplikacji (AppID GUID), struktury SECURITY_DESCRIPTOR lub interfejs IAccessControl. Jeżeli parametr ten ma wartość NULL, COM przydzieli uprawnienia wszystkim użytkownikom - taki sam efekt daje określenie w programie DCOMCnfg konta WSZYSCY (Everyone). Jeżeli parametr ten jest niepustym wskaźnikiem, piąty parametr (dwAuthnLevel) nie może mieć wartości RPC_C_AUTH_LEVEL_NONE.
Drugi i trzeci parametr - cAuthSvc i asAuthSvc - również odnoszą się do serwera: pierwszy z nich jest licznikiem, drugi tablicą następujących struktur, reprezentujących pakiety uwierzytelniające:
typedef struct tagSOLE_AUTHENTICATION_SERVICE
{
DWORD dwAuthnSvc;
DWORD dwAuthzSvc;
OLECHAR __RPC_FAR *pPrincipalName;
HRESULT hr;
} SOLE_AUTHENTICATION_SERVICE;
Przypisując parametrom wartości cAuthSvc=-1 i asAuthSvc=NULL, powodujemy wykorzystanie usług ogólnosystemowych. W Windows NT 4.0 jedyną dostępną usługą uwierzytelniającą jest NTLM (NT LAN Manager), w Windows 2000 dostępny jest ponadto Kerberos.
Piąty parametr - dwAuthnLevel - określa poziom uwierzytelnienia i dotyczy zarówno klienta, jak i serwera; odpowiada to ustawieniom specyficznym dla serwera w programie DCOMCnfg (patrz rys. 13.5). Wartość tego parametru nie może być zmieniana poniżej wartości określonej przez program DCOMCnfg, w przeciwnym razie wywołanie zakończy się niepowodzeniem.
Szósty parametr - dwImplLevel - odnosi się tylko do klienta przy jego odwołaniach do serwera. Nie ma on odpowiednika w ustawieniach programu DCOMCnfg (związanego tylko z ustawieniami serwera), może jednak przyjmować identyczne wartości, jak analogiczny parametr serwera (rys. 13.2):
RPC_C_IMP_LEVEL_DEFAULT
RPC_C_IMP_LEVEL_ANONYMOUS
RPC_C_IMP_LEVEL_IDENTIFY
RPC_C_IMP_LEVEL_IMPERSONATE
RPC_C_IMP_LEVEL_DELEGATE
Ósmy parametr - dwCapabilities - dotyczy klienta i serwera i określa ich dodatkowe możliwości. Może on stanowić sumę logiczną następujących znaczników:
EOAC_NONE (=0) - oznacza brak jakichkolwiek znaczników;
EOAC_MUTUAL_AUTH (=1) - nie używane; wielokrotne uwierzytelnianie (mutual authentication) oferowane jest automatycznie przez niektóre usługi uwierzytelniające;
EOAC_SECURE_REFS(=2) - oznacza, iż wywołania rejestrowane przez licznik odwołań muszą być uwierzytelniane, by uniknąć błędnych zwolnień zasobów;
EOAC_ACCESS_CONTROL(=4)- ustawienie tego znacznika oznacza, że pierwszy parametr (pSecDesc) zawiera wskazanie na interfejs IAccessControl;
EOAC_APPID (=8) - ustawienie tego znacznika oznacza, że pierwszy parametr (pSecDesc) zawiera wskazanie na GUID aplikacji. Funkcja CoInitializeSecurity() odszukuje w Rejestrze wskazany GUID i odczytuje związane z nim ustawienia zabezpieczeń.
Wykorzystanie CoInitializeSecurity()
Zmodyfikujemy teraz naszą przykładową aplikację tak, by ustawienia zabezpieczeń dokonywane były przez funkcję wymienioną w tytule. Uczyni to naszą aplikację zdolną do komunikowania się przez Internet - w tym celu należy dopuścić dostęp anonimowych użytkowników oraz wyłączyć uwierzytelnianie i uosobienie; da się to zrobić w prosty sposób właśnie dzięki funkcji CoInitializeSecurity().
Jak już wcześniej napisaliśmy, funkcja CoInitializeSecurity() musi być wywołana dla danego procesu tylko raz, przez pierwszym odwołaniem się do obiektu COM; jej wywołanie musi nastąpić zarówno po stronie klienta, jak i po stronie serwera po wywołaniu CoInitialize().
W tym celu:
Otwórz projekt EasyDCOM.bpr i wyświetl jego główny plik źródłowy (Project|View Source).
Uzupełnij następująco funkcję WinMain() (wyblakły druk oznacza kod już istniejący):
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
CoInitialize(NULL);
CoInitializeSecurity(
NULL,
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_NONE,
RPC_C_IMP_LEVEL_ANONYMOUS,
NULL,
EOAC_NONE,
NULL
);
try
{
Application->Initialize();
Application->Title = "Easy DCOM Server";
Application->CreateForm(__classid(TfrmMain), &frmMain);
Application->Run();
}
catch (Exception &exception) {
Application->ShowException(&exception);
}
CoUninitialize();
return 0;
}
Otwórz projekt EasyDCOMClient.bpr, wyświetl jego główny plik źródłowy (EasyDCOMClient.cpp) i zmodyfikuj funkcję WinMain() identycznie jak w pkt. 2.
Dodaj do modułu EasyDCOMClient.cpp dyrektywę dołączającą plik nagłówkowy objbase.h:
#include <vcl.h>
#include <objbase.h>
#pragma hdrstop
Wywołanie funkcji CoInitialize() spowoduje, że odwołanie do serwera zostanie zrealizowane w ramach wspólnego apartamentu modelu wątkowego STA (patrz rozdział 12.). Funkcja CoInitializeSecurity() ustala następnie odpowiednie parametry zabezpieczeń.
I tak ustawienie pierwszego parametru na NULL oznacza, iż dostęp do serwera nie jest kontrolowany. Dwa następne parametry (licznik = -1, wskaźnik tablicy = NULL) informują o nieobecności dodatkowych pakietów uwierzytelniających, co spowoduje przyjęcie w tym względzie ustawień ogólnosystemowych. Ponieważ nie jesteśmy zainteresowani uwierzytelnianiem odwołań do serwera, parametr dwAuthnLevel ma wartość RPC_C_AUTHN_LEVEL_NONE. Nie znając użytkownika łączącego się z serwerem przez Internet, nie jesteśmy także zainteresowani jego uosabianiem na serwerze i działaniem w jego imieniu, tak więc parametr dwImpLevel ma wartość RPC_C_IMP_LEVEL_ANONYMOUS. Wreszcie - charakter naszego serwera nie wymaga używania żadnego ze znaczników związanych z ostatnim parametrem, nadaliśmy więc temu parametrowi wartość zerową (EOAC_NONE).
Serwery DCOM używające punktów połączeniowych również powinny zlikwidować swe zabezpieczenia w opisany sposób. Gdybyśmy chcieli uruchomić przykładowy serwer z rozdziału 12. jako zdalny serwer DCOM, skończyłoby się to komunikatem o odmowie dostępu (Access Denied). Stałoby się tak dlatego, iż klient próbowałby weryfikować uprawnienia serwera próbującego uzyskać dostęp do jego ujścia zdarzeń.
Można by obejść ten problem w inny sposób, utożsamiając kontro serwera z kontem klienta - innymi słowy użytkownik - klient na komputerze - kliencie musiałby mieć taką samą nazwę i hasło jak użytkownik określony na karcie Identyfikacja (patrz rysunek 13.8). Problem w tym, iż wymaga to wcześniejszej znajomości użytkownika chcącego uruchomić aplikację klienta.
CoInitializeSecurity() a serwer wewnątrzprocesowy
Cóż jednak począć, gdy chcielibyśmy zaimplementować klienta DCOM w postaci kontrolki ActiveX przeznaczonej do wykorzystania w ramach jakiejś aplikacji - kontenera, jak np. Internet Explorer czy IIS i Active Server Pages?
Serwer wewnątrzprocesowy, będący biblioteką DLL, ładowany jest do przestrzeni adresowej aplikacji - kontenera; funkcja CoInitializeSecurity() wywoływana jest przez COM przed tym załadowaniem (jawnie lub automatycznie, wszystko jedno). Nawet jeżeli zostanie ona wywołana w kodzie klienta, i tak będzie już za późno - wszelkie ustawienia zabezpieczeń będą już wówczas sprecyzowane i nie będzie można ich zmienić.
Jednym z możliwych rozwiązań tego problemu jest stworzenie dodatkowego zewnątrzprocesowego (EXE) serwera pośredniczącego, odwołującego się do zdalnego serwera DCOM w imieniu klienta i traktowanego przez tego ostatniego jako swoisty reprezentant serwera zdalnego. Nie zmienia to jednak faktu, iż z poziomu biblioteki DLL nie jest możliwe kontrolowanie ustawień zabezpieczających przez wywołanie funkcji CoInitializeSecurity(); należy o tym pamiętać, decydując się na tworzenie klienta zdalnego serwera DCOM w postaci biblioteki DLL.
Implementowanie programowej kontroli dostępu
Możliwe jest selektywne udzielanie zezwoleń na dostęp do serwera użytkownikom lub ich grupom. Pierwszy parametr wywołania funkcji CoInitializeSecurity() jest wówczas poprawnym wskaźnikiem, zaś parametr dwAuthnLevel musi mieć wartość nie mniejszą niż RPC_C_AUTH_LEVEL_CONNECT, w przeciwnym razie wywołanie funkcji zakończy się błędem.
Jednym z możliwych obiektów wskazywanych przez pierwszy parametr jest struktura tzw. deskryptora zabezpieczeń:
typedef struct _SECURITY_DESCRIPTOR {
BYTE Revision;
BYTE Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control;
PSID Owner;
PSID Group;
PACL Sacl;
PACL Dacl;
} SECURITY_DESCRIPTOR;
Bezpośrednie operowanie deskryptorami zabezpieczeń za pomocą funkcji Win32 API nie jest czynnością zbyt wygodną; C++Builder 5 wraz ze swą biblioteką ATL oferuje więc w tym celu specjalną klasę CSecurityDescriptor. Może być ona używana wszędzie tam, gdzie wymagana jest struktura SECURITY_DESCRIPTOR, dzięki zdefiniowanemu operatorowi konwersji:
class CSecurityDescriptor
{
…
public:
…
…
operator PSECURITY_DESCRIPTOR()
{
return m_pSD;
}
…
public:
PSECURITY_DESCRIPTOR m_pSD;
…
Programowa modyfikacja list kontroli dostępu (ACL - Access Control Lists) za pomocą funkcji Win32 Security API jest czynnością złożoną, wymagającą ostrożnego kodowania i testowania. Począwszy od Windows NT 4.0 z drugą poprawką (Service Pack 2) COM udostępnia jako alternatywę interfejs IAccessControl, implementowany przez obiekt systemowy CLSID_DCOMAccessControl.
Wbudowane konto SYSTEM musi być bezwarunkowo włączone do list kontroli dostępu, ponieważ Menedżer Sterowania Usługami (System Service Control Manager) wymaga tego do zarządzania serwerami COM.
Dla celów ilustracji programowego ustawiania kontroli dostępu zmodyfikujemy nasz serwer tak, by dostęp do niego mieli jedynie klienci używający konta Guest; konto to jest nieaktywne po pierwszej instalacji systemu.
Otwórz więc projekt serwera i do pliku EasyDCOM.cpp dodaj dyrektywę włączającą plik atlcom.h:
#include <vcl.h>
#pragma hdrstop
#include <atl\atlmod.h>
#include <atl\atlcom.h>
#include "HostInfoImpl.h"
Przed wierszem zawierającym wywołanie funkcji CoInitialize() (w funkcji WinMain()) dodaj następujący blok kodu:
CSecurityDescriptor sd;
sd.InitializeFromProcessToken();
sd.Allow("Guest", COM_RIGHTS_EXECUTE);
sd.Allow("NT_AUTHORITY\\SYSTEM", COM_RIGHTS_EXECUTE);
Nowo utworzony obiekt sd klasy CSecurityDescriptor inicjowany jest tutaj za pomocą metody InitializeFromProcessToken(), dokonującej ustawienia jego wewnętrznych danych w kontekście bieżącego procesu. Dwa wywołania metody Allow() ustanawiają zezwolenie na uruchamianie serwera w stosunku do użytkownika Guest oraz systemowego konta NT_AUTHORITY\SYSTEM.
Zmień następnie parametry wywołania funkcji CoInitializeSecurity() w następujący sposób:
CoInitializeSecurity(
sd, // tu zmiana
-1,
NULL,
NULL,
RPC_C_AUTHN_LEVEL_CONNECT, // tu zmiana
RPC_C_IMP_LEVEL_IDENTIFY, // tu zmiana
NULL,
EOAC_NONE,
NULL
)
Przejdź teraz do projektu klienta, otwórz plik EasyDCOMClient.cpp i usuń wiersze funkcji WinMain(), zawierające wywołania funkcji CoInitialize(), CoInitializeSecurity() i CoUninitialize().
Abyś mógł wypróbować działanie zmodyfikowanych projektów, musisz mieć uprawnienia posługiwania się kontem Guest i używać takiego samego hasła na komputerze klienta i serwera. Możesz też zalogować się jako Guest na komputerze serwera i uruchomić na nim aplikację klienta. Próba uruchomienia aplikacji klienta z konta innego niż Guest spowoduje odrzucenie przez serwer próby dostępu.
Nazwę Guest można zastąpić nazwą dowolnego konta lokalnego lub domenowego; przekazując tę nazwę do metody Allow() klasy CSecurityDescriptor musisz nadać jej format <Domena>\\<UG> albo <Komputer>\\UG>, gdzie <Domena> i <Komputer> oznaczają nazwę (odpowiednio) domeny i komputera, zaś <UG> jest nazwą użytkownika albo grupy.
Windows 2000 oferuje nowy program użytkowy o nazwie RunAs, pozwalający uruchomić aplikację w kontekście uprawnień wskazanego konta. Stwarza to ogromne ułatwienie dla testowania serwerów COM pod względem uruchamiania ich przez różnych klientów.
Bezpieczeństwo interfejsu
Do tej pory zajmowaliśmy się zabezpieczeniami procesu jako całości, wykorzystując w tym celu funkcje CoInitializeSecurity(). Niekiedy jednak zabezpieczenia powinny mieć charakter bardziej selektywny, na przykład wymagając szyfrowania danych w stosunku do wywołania tylko jednej, wskazanej metody, nie zaś całego procesu.
COM umożliwia nam taką selektywną kontrolę zabezpieczeń, udostępniając w tym celu dwa interfejsy. Po stronie klienta warstwa programowa, reprezentowana przez menedżera proxy (Proxy Manager), implementuje metody interfejsu IClientSecurity. Analogiczna warstwa po stronie serwera - Stub Manager - implementuje interfejs IServerSecurity.
Interfejs IClientSecurity
Na interfejs IClientSecurity składają się trzy metody: QueryBlanket(), SetBlanket() i CopyProxy().
Metoda QueryBlanket() uzyskuje informację uwierzytelniającą przekazaną w wywołaniu funkcji CoInitializeSecurity() przy inicjowaniu procesu i używaną przez proxy. SetBlanket() konfiguruje taką informację na użytek proxy, zaś metoda CopyProxy() tworzy prywatną kopię proxy w celu późniejszego jej użycia przez metodę SetBlanket().
Parametry wywołania metod QueryBlanket() i SetBlanket() odpowiadają poszczególnym parametrom wywołania funkcji CoInitializeSecurity(), z jednym wszakże istotnym wyjątkiem, którym jest siódmy parametr - pAuthInfo. Stanowi on wskaźnik na strukturę zależną od używanych pakietów - gdy używane są pakiety NTLM, strukturą tą jest COAUTHIDENTITY:
typedef struct _COAUTHIDENTITY
{
USHORT *User;
ULONG UserLength;
USHORT *Domain;
ULONG DomainLength;
USHORT *Password;
ULONG PasswordLength;
ULONG Flags;
} COAUTHIDENTITY;
Jak łatwo się domyślić, poszczególne pola struktury odzwierciedlają określone konto - nazwę użytkownika lub domeny oraz hasło. Jeżeli parametr pAuthInfo jest pustym wskaźnikiem, wywołania metod uwierzytelniane są na zasadach określonych dla całego procesu klienta.
Aby wywołać jakąś metodę interfejsu IClientSecurity, należy najpierw zażądać jego egzemplarza, wywołując metodę QueryInterface() menedżera proxy, wywołać żądaną metodę, a następnie zwolnić ów egzemplarz za pomocą metody Release(). COM udostępnia otoczki metod interfejsu IClientSecurity automatycznie realizujące tę sekwencję - noszą one nazwy (odpowiednio) CoQueryproxyBlanket(), CoSetProxyBlanket() i CoCopyProxy().
Interfejs IServerSecurity
Odpowiednikiem interfejsu IClientSecurity po stronie serwera jest IServerSecurity odpowiedzialny za uosobienie klienta. Wskaźnik na jego egzemplarz dostępny jest w treści wywoływanych metod za pośrednictwem funkcji CoGetCallContext().
Interfejs IServerSecurity posiada cztery metody: QueryBlanket(), ImpersonateClient(), RevertToSelf() oraz IsImpersonating(); dla pierwszych trzech COM udostępnia otoczki o nazwach (odpowiednio): CoQueryClientBlanket(), CoImpersonateClient() i CoRevertToSelf().
Metoda QueryBlanket() jest odpowiednikiem swej partnerki po stronie klienta: udostępnia bieżące ustawienia zabezpieczeń. Za jej pomocą można na przykład dowiedzieć się, czy klient stosuje szyfrowanie bądź też uzyskać dane tegoż klienta.
Zadaniem metody ImpersonateClient() jest przyjęcie przez serwer uosobienia określonego klienta. Poziom tego uosobienia decyduje o tym, czy serwer ma prawo dostępu do obiektów systemowych, czy też tylko wykonać może sprawdzenie uprawnień dostępu.
Wywołanie metody RevertToSelf() anuluje uosobienie będące konsekwencją wywołania metody ImpersonateClient(). COM automatycznie wywołuje metodę RevertToSelf() po każdym powrocie z wywoływanej metody serwera.
Do sprawdzenia, czy serwer znajduje się aktualnie w stanie uosabiającym jakiegoś klienta, służy metoda IsImpersonating().
Wykorzystanie blankietu
Działanie interfejsów IServerSecurity i IClientSecurity zilustrujemy za pomocą kolejnej pary projektów. Działająca aplikacja kliencka dokonywać będzie zmian w uprawnieniach użytkownika, a następnie zwracać się będzie do serwera z poleceniem rozpoznania ich bieżącego stanu; serwer będzie także miał możliwość zapisywania ustawień klienta w zewnętrznym pliku.
Przed rozpoczęciem eksperymentu należy założyć w systemie dwa lokalne konta: CommonUser i ExtraUser, będące członkami grupy User, na obydwu komputerach - serwera i klienta. Logowanie na komputerze klienta odbywać się będzie z użyciem konta ExtraUser. Za pomocą programu DCOMCnfg należy ponadto skonfigurować serwer tak, by był dostępny dla dowolnego użytkownika (Wszyscy lub EveryOne) i ustawić identyfikację serwera (patrz rysunek 13.8) na użytkownika Administrator.
Kompletny kod obydwu projektów znajduje się na załączonej płycie CD-ROM; tutaj prześledzimy pokrótce proces ich budowy.
Rozpocznijmy od projektu serwera:
Zainicjuj nową aplikację i nadaj projektowi nazwę Blanket.bpr. Z okna New Items wybierz pozycję Automation Object i nadaj jego koklasie nazwę ObjBlanket. Posługując się następnie edytorem TLE, dodaj do serwera dwie następujące metody:
[id(1)] HRESULT _stdcall BlanketInfo([out, retval] BSTR * Value);
[id(2)] HRESULT _stdcall CreateFile([in] BSTR value);
Otwórz plik ObjBlanketImpl.cpp i wpisz następującą treść metody BlanketInfo():
STDMETHODIMP TObjBlanketImpl::BlanketInfo(BSTR* Value)
{
try {
*Value = NULL;
LPWSTR pPrivs;
OleCheck(CoQueryClientBlanket(NULL, NULL, NULL, NULL, NULL,
(LPVOID*)&pPrivs, NULL));
WideString strInfo = pPrivs;
*Value = strInfo.Detach();
}
catch(Exception &e) {
return Error(e.Message.c_str(), IID_IObjBlanket);
}
return S_OK;
};
Powyższa funkcja wywołuje funkcję CoQueryClientBlanket() w celu uzyskania nazwy użytkownika i zwrócenia jej do wywołującego. Wszystkie inne elementy informacji o użytkowniku nie są w tej chwili interesujące, dlatego reprezentujące je parametry są pustymi wskaźnikami.
W tym samym pliku wpisz kod implementujący metodę CreateFile():
STDMETHODIMP TObjBlanketImpl::CreateFile(BSTR Value)
{
try {
OleCheck(CoImpersonateClient());
HANDLE hFile = ::CreateFile(AnsiString(Value).c_str(),
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == hFile)
return HRESULT_FROM_WIN32(GetLastError());
CloseHandle(hFile);
OleCheck(CoRevertToSelf());
}
catch(Exception &e) {
return Error(e.Message.c_str(), IID_IObjBlanket);
}
return S_OK;
};
Wywołując funkcję CoImpersonateClient(), uosabiamy na serwerze bieżącego klienta - jak pamiętamy, zgodnie z oryginalnymi ustawieniami serwer pracuje w kontekście konta Administrator; dane uosobionego klienta zapisywane są do lokalnego pliku, po czym następuje powrót serwera do oryginalnego kontekstu, czyli do konta Administrator.
Po skompilowaniu projektu skopiuj jego plik wynikowy Blanket.exe na zdalny komputer i uruchom jednokrotnie w celu zarejestrowania.
Scenariusz budowy aplikacji - klienta przedstawia się następująco:
Zainicjuj nową aplikację i zapisz jej pliki pod nazwami BlanketClient.bpr i MainUnit.cpp.
Zaprojektuj formularz główny w sposób pokazany na rysunku 13.15, nadając jego poszczególnym komponentom następujące nazwy:
Nazwa oryginalna |
Nowa nazwa |
Form1 |
frmMain |
RadioButton1 |
rdoLoged |
RadioButton2 |
rdoIdnty |
GroupBox1 |
gbxIdn |
GroupBox2 |
gbxInfo |
Label1 |
lblUser |
Label2 |
lblDomain |
Label3 |
lblPwrd |
Edit1 |
txtUser |
Edit2 |
txtDomain |
Edit3 |
txtPwrd |
Edit4 |
txtInfo |
Button1 |
cmdCallServer |
Button2 |
cmdSetIdentity |
Button3 |
cmdGetInfo |
Button4 |
cmdCreateFile |
Button5 |
cmdFinish |
Tu proszę wkleić rysunek z pliku orig-17-15.bmp
Rysunek 13.15. Formularz główny aplikacji klienta
Otwórz plik MainUnit.h i dodaj do niego dyrektywę:
#include "..\server\Blanket_TLB.h"
Dodaj do części prywatnej deklaracji formularza TfrmMain dwa następujące pola:
TCOMIObjBlanket m_objBlanket;
SEC_WINNT_AUTH_IDENTITY m_AuthInfo;
Dodaj do części prywatnej deklaracji formularza TfrmMain deklarację dwóch następujących metod:
void GetAuthInfo();
void SetAuthInfo();
Kompletny kod implementacyjny głównego formularza, zawarty w pliku MainUnit.cpp, prezentujemy na wydruku 13.1:
Wydruk 13.1. Implementacja modułu głównego formularza aplikacji - klienta
#include <vcl.h>
#pragma hdrstop
#include "MainUnit.h"
#pragma package(smart_init)
#pragma resource "*.dfm"
TfrmMain *frmMain;
__fastcall TfrmMain::TfrmMain(TComponent* Owner) : TForm(Owner)
{
m_AuthInfo.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI;
m_AuthInfo.User = NULL;
m_AuthInfo.Domain = NULL;
m_AuthInfo.Password = NULL;
}
void __fastcall TfrmMain::FrmMain_Close(TObject *Sender, TCloseAction &Action)
{
m_objBlanket.Unbind();
delete[] m_AuthInfo.User;
delete[] m_AuthInfo.Domain;
delete[] m_AuthInfo.Password;
}
void __fastcall TfrmMain::CmdCallServer_Click(TObject *Sender)
{
if (!m_objBlanket.IsBound()) {
AnsiString strHost;
if (InputQuery("Create Server", "Enter computer name:", strHost)) {
if (strHost.IsEmpty())
OleCheck(CoObjBlanket::Create(m_objBlanket));
else
OleCheck(CoObjBlanket::CreateRemote(WideString(strHost),
m_objBlanket));
ShowMessage("Server created");
}
}
}
void __fastcall TfrmMain::CmdCreateFile_Click(TObject *Sender)
{
if (m_objBlanket.IsBound()) {
AnsiString strFile;
if (InputQuery("Create File", "Enter path and file name:", strFile)) {
OleCheck(m_objBlanket.CreateFile(WideString(strFile)));
ShowMessage("File created");
}
}
}
void __fastcall TfrmMain::CmdFinish_Click(TObject *Sender)
{
Close();
}
void __fastcall TfrmMain::CmdGetInfo_Click(TObject *Sender)
{
if (m_objBlanket.IsBound())
GetAuthInfo();
}
void __fastcall TfrmMain::CmdIdentity_Click(TObject *Sender)
{
gbxIdn->Enabled = rdoIdnty->Checked ? true : false;
txtDomain->Color = rdoIdnty->Checked ? clWindow : clBtnFace;
txtPwrd ->Color = rdoIdnty->Checked ? clWindow : clBtnFace;
txtUser ->Color = rdoIdnty->Checked ? clWindow : clBtnFace;
}
void __fastcall TfrmMain::CmdSetIdentity_Click(TObject *Sender)
{
if (m_objBlanket.IsBound()) {
if (rdoIdnty->Checked)
SetAuthInfo();
OleCheck(CoSetProxyBlanket(m_objBlanket,
RPC_C_AUTHN_WINNT,
RPC_C_AUTHZ_NONE,
NULL,
RPC_C_AUTHN_LEVEL_CONNECT,
RPC_C_IMP_LEVEL_IMPERSONATE,
rdoIdnty->Checked ? &m_AuthInfo :NULL,
EOAC_NONE));
}
}
void TfrmMain::GetAuthInfo()
{
WideString strInfo;
OleCheck(m_objBlanket.BlanketInfo(&strInfo));
txtInfo->Text = strInfo;
}
void TfrmMain::SetAuthInfo()
{
delete[] m_AuthInfo.User;
delete[] m_AuthInfo.Domain;
delete[] m_AuthInfo.Password;
m_AuthInfo.UserLength = txtUser ->Text.Length();
m_AuthInfo.DomainLength = txtDomain->Text.Length();
m_AuthInfo.PasswordLength = txtPwrd ->Text.Length();
m_AuthInfo.User = (PUCHAR)new TCHAR[txtUser ->Text.Length()+1];
m_AuthInfo.Domain = (PUCHAR)new TCHAR[txtDomain->Text.Length()+1];
m_AuthInfo.Password = (PUCHAR)new TCHAR[txtPwrd ->Text.Length()+1];
lstrcpy((LPTSTR)m_AuthInfo.User , (LPCTSTR)txtUser ->Text.c_str());
lstrcpy((LPTSTR)m_AuthInfo.Domain , (LPCTSTR)txtDomain->Text.c_str());
lstrcpy((LPTSTR)m_AuthInfo.Password, (LPCTSTR)txtPwrd ->Text.c_str());
}
Jak widzimy na prezentowanym wydruku, niektóre pola struktury m_AuthInfo inicjowane są w konstruktorze formularza. Zgodnie z plikiem pomocy C++Buildera 5 „OLE Programmer's Reference” COM utrzymuje wskaźnik na tę strukturę aż do momentu swej deinicjalizacji (wartość tego wskaźnika może być zmieniana).
Metoda SetAuthInfo() dokonuje wpisania nowych wartości we wspomnianą strukturę m_AuthInfo, zwalniając jednocześnie pamięć zajmowaną przez informację dotychczasową i zapobiegając w ten sposób wyciekom pamięci.
Przycisk „Set Identify” umożliwia zmianę identyfikacji klienta. Jego funkcja zdarzeniowa wywołuje funkcję CoSetProxyBlanket(), informującą Proxy Managera, iż należy zmienić kontekst użytkownika zgodnie z przekazaną strukturą. Wartość wskaźnika reprezentującego wspomnianą strukturę zależna jest od stanu opcji w górnej części formularza i jest albo wskazaniem struktury m_AuthInfo, albo wskazaniem pustym.
Funkcja zdarzeniowa przycisku „Get Info” wywołuje metodę BlanketInfo() serwera w celu uzyskania identyfikacji aktualnego klienta i wpisania jej w pole txtInfo.
Kliknięcie przycisku „Create File” spowoduje utworzenie pliku na zdalnym komputerze. Funkcja zdarzeniowa wyświetla najpierw dialog prowadzący do pobrania nazwy pliku, a następnie wywołuje metodę CreateFile() serwera.
Zakończyliśmy konstruowanie naszych projektów, sprawdźmy więc, jak ze sobą współpracują:
Uruchom program BlanketClient.exe i kliknij przycisk „Call Server”. W wyświetlonym oknie dialogowych wpisz nazwę zdalnego komputera - spowoduje to uruchomienie serwera.
Kliknij przycisk „Get Info”; w polu „Client Name” pojawi się nazwa aktualnie zalogowanego użytkownika (rys. 13.16).
Tu proszę wkleić rysunek z pliku orig-17-16.bmp
Rysunek 13.16. Klient odwołujący się do serwera w kontekście aktualnie zalogowanego użytkownika
Kliknij przycisk „Create File”, wprowadź dowolną nazwę w wyświetlone okno dialogowe i zatwierdź ją. Następnie obejrzyj właściwości utworzonego pliku i zauważ, kto jest jego właścicielem (rys. 13.17).
Tu proszę wkleić rysunek z pliku orig-17-17.bmp po uprzednim oczyszczeniu go ze zbędnego tła na drugim planie, by widoczne były tylko dwa okna jak na rysunku 17.17 w oryginale na stronie 747.
Rysunek 13.17. Właścicielem utworzonego pliku jest użytkownik zalogowany
Wybierz opcję „Use this identity”, wpisz w pole „Name” nazwę CommonUser i kliknij przycisk „Set Identity”. W polu „Client Name” pojawi się nazwa uosobionego użytkownika (rys. 13.18).
Tu proszę wkleić rysunek z pliku orig-17-18.bmp
Rysunek 13.18. Klient odwołujący się do serwera z uosobionym użytkownikiem
Ponownie utwórz plik (o innej nazwie niż w pkt. 3.) i sprawdź teraz, czyją jest on własnością (rys. 13.19).
Tu proszę wkleić rysunek z pliku orig-17-19.bmp po uprzednim oczyszczeniu go ze zbędnego tła na drugim planie, by widoczne były tylko dwa okna jak na rysunku 17.19 w oryginale na stronie 748.
Rysunek 13.19. Właścicielem utworzonego pliku jest użytkownik uosobiony na serwerze
Podsumowanie
Mechanizmy DCOM są tematem na tyle obszernym, iż ich wyczerpujące przedstawienie wymagałoby nie tyle osobnej książki, ile raczej potężnego księgozbioru. Z konieczności więc skupiliśmy się w tym rozdziale na wybranych zagadnieniach, z uwzględnieniem ich praktycznego charakteru.
Rozpoczęliśmy od elementów, które czynnik rozproszenia wprowadza do oryginalnej technologii COM - głównie pod kątem bezpieczeństwa aplikacji. Zaprezentowaliśmy przykład wykorzystania programu DCOMCnfg, stanowiącego narzędzie konfiguracyjne dla mechanizmów DCOM w środowisku Windows.
W uzupełnieniu do deklaratywnych elementów zabezpieczeń, konfigurowanych przez program DCOMCnfg, przedstawiliśmy także przykładowe zastosowanie zabezpieczenia programowanego na podstawie funkcji CoInitializeSecurity() oraz interfejsów IClientSecurity i IServerSecurity.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 D:\helion\C++Builder 5\R13-03.DOC