Я увидел интересный ответ в обсуждении на форуме, в котором я участвовал. Вопрос был не на тему этого поста, но ответ с кодом был достаточно интересным, чтобы написать эту статью. Код был написан на Delphi, но, очевидно, писался в стиле простого C (даже с использованием goto!).
Однако сейчас я хочу поговорить о том, как WinAPI обычно возвращает ошибки, и почему код в ответе - не самый удачный способ.
Тот код выглядел не в точности так. Я просто выделил из него существенные части для вашего удобства (исходный код напоминал C ещё больше, чем этот пример):
function IsUserMemberOf(const Group : PSID) : BOOL;
var
hToken : HANDLE;
begin
result := false;
// получаем Token и сохраняем в hToken ... (короткий вариант)
if not OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY or TOKEN_DUPLICATE, hToken) then
goto Exit;
// дополнительные действия для Vista и много-много gotos
if not CheckTokenMembership(hToken, Group, @result) then
begin
goto Exit_1;
end;
:Exit_1
CloseHandle(hToken);
goto Exit;
:Exit
end;
В чём проблема с возвращаемым значением этой функции? Во-первых, возвращаемое значение каждой WinAPI функции проверяется и функция возвращает управление, если вызываемая WinAPI функция возвращает ошибку. Это - очень хорошо, потому что я видел кучу кода, который ничем таким не озаботились. Но здесь есть другая проблема. Существует два возможных набора результатов, которые мы бы хотели сообщить нашему вызывающему:
- Является ли пользователь членом группы. Это, очевидно, двоичное значение (скажем,
True/False). - Произошла ли ошибка. Аналогично, это двоичное значение (
True/False).
False(0) до True(1). Функция проецирует два набора с разным смыслом в один набор - результат функции. Если один из вызовов WinAPI завершиться неудачей, то функция выйдет и вернёт False. Очевидно, что значение False используется для двух совершенно разных по смыслу результатов. Одно из которых говорит вызывающему, что пользователь не входит в группу, а другое - что функция почему-то завершилась с ошибкой. А мы соединили эти два смысла в одно значение. Т.е. мы потеряли информацию! Теперь мы не можем отличить один случай от другого без дополнительной (мета) информации. Всегда думайте об этом, когда вы проектируете заголовок функции. Но, конечно же, эта информация есть у нас в другом месте. В самом WinAPI. GetLastError даст вам значение ошибки, которое (я надеюсь) позволит нам решить нашу проблему. Так что всё нормально? Хотелось бы верить! Посмотрите на это:
function GetMyUserName : String;
var
nSize : Cardinal;
p : PChar;
begin
nSize := 0;
if not GetUserName(nil, nSize) and (GetLastError() = ERROR_INSUFFICIENT_BUFFER) then
begin
GetMem(p, nSize * sizeof(CHAR));
try
if not GetUserName(p, nSize) then
RaiseLastOSError;
result := p;
finally
FreeMem(p);
end;
end
else
RaiseLastOSError;
end;
begin
...
xy := GetMyUserName();
bIsMember := IsUserMemberOf(aGroup);
if not bIsMember and (GetLastError() <> 0) then
ОШИБКА //...обработка;
end;
Функция IsUserMemberOf вызывается после успешного вызова GetMyUserName, которая первым делом просит WinAPI-функцию GetUserName вернуть размер строки, содержащей имя пользователя. В этом случае GetLastError вернёт ERROR_INSUFFICIENT_BUFFER (122dec), которая говорит нам, что необходимо использовать больший блок памяти.В итоге
GetMyUserName вернёт нам имя пользователя (успешно). Предположим, что IsUserMemberOf также будет успешна, но при этом она говорит нам, что пользователь не входит в группу. Тогда возвращаемое значение будет False, а тот факт, что большинство WinAPI функций не изменяют значение LastError при успешном вызове, говорит нам, что GetLastError всё ещё будет равно ERROR_INSUFFICIENT_BUFFER (122dec): поэтому когда мы будем проверять bIsMember - мы попадём на ветку кода с обработкой ошибки, хотя никакой ошибки не было.Как вы видите, даже хотя мы можем получить дополнительные сведения от
GetLastError, но эта функция реализована так, что мы не можем воспользоваться этой информацией.Если вы планируете написать такую функцию, я призываю вас не использовать техники языка C. Почему? Просто посмотрите как выглядит этот стиль:
function GetUserNameW(lpBuffer: LPWSTR; nSize: PDWORD): BOOL;
var
pUserName: PWideChar;
res: NTSTATUS;
bRes: Boolean;
begin
bRes := FALSE;
if (nSize = nil) then
begin
SetLastError(ERROR_INVALID_PARAMETER);
goto Exit;
end;
if (lpBuffer = NIL) or (nSize^ <= InternalGetMinUserNameLength()) then
begin
nSize^ := InternalGetMinUserNameLength();
SetLastError(ERROR_INSUFFICIENT_BUFFER);
goto Exit;
end;
// Это не совсем нужно, но просто добавить сложности в пример
pUserName := HLOCAL(LocalAlloc(LPTR, InternalGetMinUserNameLength() * sizeof(WIDECHAR));
// TODO: проверить pUserName на nil: если nil - goto Exit
res := NtGetUserName(pUserName, InternalGetMinUserNameLength() * sizeof(WIDECHAR));
if NT_FAILED(res) then
begin
SetLastError(NtStatusToDosError(res));
goto Exit_1;
end;
if not StrCopyMemory(lpBuffer, pUserName) then
goto Exit_1;
bRes := TRUE;
:Exit_1
FreeMem(pUserName);
goto Exit;
:Exit
result := bRes;
end;
Выглядит сложно, не так ли? Я не собирался писать точную реализацию GetUserName (положа руку на сердце - я её не знаю), а вместо этого показал возможную реализацию. Так что WinAPI использует результат функции только как указание на ошибку. False означает провал операции, а GetLastError сообщит вам, что именно пошло не так. True означает, что всё в порядке.Нам действительно нужно всё это в Delphi? Вообще-то нет. Если вы хотите написать библиотеку (DLL), которую нужно вызывать из других языков, то вы можете использовать
SetLastError. И вам придётся возвращать результат функции в var/out параметре (по ссылке). Конечно же, вы также можете использовать тип HRESULT в качестве возвращаемого значения.
function GetUserNameW(lpBuffer: LPWSTR; nSize: PDWORD): HRESULT;Если же вы хотите написать просто функцию для Delphi, то вы можете использовать исключения.
procedure GetUserNameW(Name: PWideChar; var nSize: DWORD);Большой плюс - что вы можете использовать функцию как функцию, поскольку результат функции теперь может прямо указывать на результат операции, а не на признак ошибки.
function GetUserNameW(Name: PWideChar; var nSize: DWORD) : PWideChar;
begin
if (nSize = nil) then
begin
SetLastError(ERROR_INVALID_PARAMETER);
RaiseLastOsError;
end;
... // не буду копировать весь код - идею вы уловили
end;
Как вы можете видеть, мы вызываем RaiseLastOsError, которая возбуждает исключение EOsError. Оно содержит код от GetLastError в своём свойстве ErrorCode. Мы даже можем создать свой собственный класс исключения (к примеру, JWSCL использует EJwsclWinCallFailedException), и вообще убрать вызов SetLastError! И, конечно же, вы можете использовать родной строковый тип Delphi, что сделает эту функцию очень простой в использовании.
function GetUserName() : String;Функция возвращает строку. Она не возвращает пустой строки при ошибке, как делали те реализации, что мы нашли в интернете. Иначе у нас была бы та же самая проблема с двумя наборами значений, спроецированными на одно.
function IsUserMemberOf(const Group : PSID) : Boolean;
var
hToken : HANDLE;
begin
result := false;
// получаем Token и сохраняем в hToken ... (короткий вариант)
if not OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY or TOKEN_DUPLICATE, hToken) then
RaiseLastOsError;
// дополнительные действия для Vista и много-много RaiseLastOsError;
if not CheckTokenMembership(hToken, Group, @result) then
RaiseLastOsError;
end;
Улучшенный вариант этой функции есть в JEDI Component Library (JCL) в файле JclSecurity.pas. Забудьте обо всех других реализациях!! Используйте эту:
function IsGroupMember(RelativeGroupID: DWORD): Boolean;
var
psidAdmin: Pointer;
Token: THandle;
Count: DWORD;
TokenInfo: PTokenGroups;
HaveToken: Boolean;
I: Integer;
const
SE_GROUP_USE_FOR_DENY_ONLY = $00000010;
begin
Result := not IsWinNT;
if Result then // на Win9x и ME нет безопасности
Exit;
psidAdmin := nil;
TokenInfo := nil;
HaveToken := False;
try
Token := 0;
HaveToken := OpenThreadToken(GetCurrentThread, TOKEN_QUERY, True, Token);
if (not HaveToken) and (GetLastError = ERROR_NO_TOKEN) then
HaveToken := OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, Token);
if HaveToken then
begin
{$IFDEF FPC}
Win32Check(AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, 2,
SECURITY_BUILTIN_DOMAIN_RID, RelativeGroupID, 0, 0, 0, 0, 0, 0,
psidAdmin));
if GetTokenInformation(Token, TokenGroups, nil, 0, @Count) or
(GetLastError <> ERROR_INSUFFICIENT_BUFFER) then
RaiseLastOSError;
TokenInfo := PTokenGroups(AllocMem(Count));
Win32Check(GetTokenInformation(Token, TokenGroups, TokenInfo, Count, @Count));
{$ELSE FPC}
Win32Check(AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, 2,
SECURITY_BUILTIN_DOMAIN_RID, RelativeGroupID, 0, 0, 0, 0, 0, 0,
psidAdmin));
if GetTokenInformation(Token, TokenGroups, nil, 0, Count) or
(GetLastError <> ERROR_INSUFFICIENT_BUFFER) then
RaiseLastOSError;
TokenInfo := PTokenGroups(AllocMem(Count));
Win32Check(GetTokenInformation(Token, TokenGroups, TokenInfo, Count, Count));
{$ENDIF FPC}
for I := 0 to TokenInfo^.GroupCount - 1 do
begin
{$RANGECHECKS OFF} // Массив [0..0] - игнорируем ERangeError
Result := EqualSid(psidAdmin, TokenInfo^.Groups[I].Sid);
if Result then
begin
// Учесть denied ACE с SID администратора
Result := TokenInfo^.Groups[I].Attributes and SE_GROUP_USE_FOR_DENY_ONLY
<> SE_GROUP_USE_FOR_DENY_ONLY;
Break;
end;
{$IFDEF RANGECHECKS_ON}
{$RANGECHECKS ON}
{$ENDIF RANGECHECKS_ON}
end;
end;
finally
if TokenInfo <> nil then
FreeMem(TokenInfo);
if HaveToken then
CloseHandle(Token);
if psidAdmin <> nil then
FreeSid(psidAdmin);
end;
end;
function IsAdministrator: Boolean;
begin
Result := IsGroupMember(DOMAIN_ALIAS_RID_ADMINS);
end;
Это же можно сделать и на JWSCL:
uses
JwaWindows,
JwsclToken,
JwsclKnownSid,
JwsclUtils;
var
Token : TJwSecurityToken;
begin
JwInitWellKnownSIDs;
Token := TJwSecurityToken.CreateTokenEffective(TOKEN_READ or TOKEN_QUERY or TOKEN_DUPLICATE);
try
Token.ConvertToImpersonatedToken(DEFAULT_IMPERSONATION_LEVEL, MAXIMUM_ALLOWED);
if Token.CheckTokenMembership(JwAdministratorsSID) then
// Пользователь - член группы Администраторы
finally
Token.Free;
end;
end;
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.