702 lines
18 KiB
C++
702 lines
18 KiB
C++
|
|
//+-----------------------------------------------------------------------
|
|
//
|
|
// Microsoft Windows
|
|
//
|
|
// Copyright (c) Microsoft Corporation 2000
|
|
//
|
|
// File: common.cxx
|
|
//
|
|
// Contents: Shared SSPI code
|
|
//
|
|
//
|
|
// History: 11-March-2000 Created Todds
|
|
//
|
|
//------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: SspAllocate
|
|
//
|
|
// Synopsis: Copies a string from the client and if necessary converts
|
|
// from ansi to unicode
|
|
//
|
|
// Effects: allocates output with either KerbAllocate (unicode)
|
|
// or RtlAnsiStringToUnicodeString
|
|
//
|
|
// Arguments: StringPointer - address of string in client process
|
|
// StringLength - Lenght (in characters) of string
|
|
// AnsiString - if TRUE, string is ansi
|
|
// LocalString - receives allocated string
|
|
//
|
|
// Requires:
|
|
//
|
|
// Returns:
|
|
//
|
|
// Notes:
|
|
//
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
PVOID
|
|
SspAllocate(
|
|
IN ULONG BufferSize
|
|
)
|
|
{
|
|
|
|
PVOID pBuffer = NULL;
|
|
if (*pSspState == SspLsaMode)
|
|
{
|
|
pBuffer = LsaFunctions->AllocateLsaHeap(BufferSize);
|
|
if (pBuffer != NULL)
|
|
{
|
|
RtlZeroMemory(Buffer, BufferSize);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ASSERT((*pSspState) == SspUserMode);
|
|
pBuffer = LocalAlloc(LPTR, BufferSize);
|
|
}
|
|
|
|
return pBuffer;
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: SspFree
|
|
//
|
|
// Synopsis: Copies a string from the client and if necessary converts
|
|
// from ansi to unicode
|
|
//
|
|
// Effects: allocates output with either KerbAllocate (unicode)
|
|
// or RtlAnsiStringToUnicodeString
|
|
//
|
|
// Arguments: StringPointer - address of string in client process
|
|
// StringLength - Lenght (in characters) of string
|
|
// AnsiString - if TRUE, string is ansi
|
|
// LocalString - receives allocated string
|
|
//
|
|
// Requires:
|
|
//
|
|
// Returns:
|
|
//
|
|
// Notes:
|
|
//
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
VOID
|
|
SspFree(
|
|
IN PVOID pBuffer
|
|
)
|
|
{
|
|
|
|
if (ARGUMENT_PRESENT(Buffer))
|
|
{
|
|
if ((*pSspState) == SspLsaMode)
|
|
{
|
|
LsaFunctions->FreeLsaHeap(Buffer);
|
|
}
|
|
else
|
|
{
|
|
ASSERT((*pSspState) == SspUserMode);
|
|
LocalFree(Buffer);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: SspCopyClientString
|
|
//
|
|
// Synopsis: Copies a string from the client and if necessary converts
|
|
// from ansi to unicode
|
|
//
|
|
// Effects: allocates output with SspAllocate, free w/ SspFree
|
|
//
|
|
// Arguments: StringPointer - address of string in client process
|
|
// StringLength - Lenght (in characters) of string
|
|
// UnicodeString - if TRUE, string is ansi
|
|
// MaxLength - Maximum length of string (useful for pwds)
|
|
// LocalString - receives allocated string
|
|
//
|
|
// Requires:
|
|
//
|
|
// Returns:
|
|
//
|
|
// Notes:
|
|
//
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
NTSTATUS
|
|
SspCopyClientString(
|
|
IN PVOID StringPointer,
|
|
IN ULONG StringLength,
|
|
IN BOOLEAN UnicodeString,
|
|
OUT PUNICODE_STRING LocalString,
|
|
IN ULONG MaxLength = 0xFFFF
|
|
)
|
|
{
|
|
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
STRING TempString = {0};
|
|
ULONG SourceSize = 0;
|
|
ULONG CharacterSize = (DoUnicode ? sizeof(WCHAR) : sizeof(CHAR));
|
|
|
|
// init outputs
|
|
LocalString->Length = LocalString->MaximumLength = 0;
|
|
LocalString->Buffer = NULL;
|
|
|
|
if (NULL != StringPointer)
|
|
{
|
|
// For 0 length strings, allocate 2 bytes
|
|
if (StringLength == 0)
|
|
{
|
|
LocalString->Buffer = (LPWSTR) SspAllocate(sizeof(WCHAR));
|
|
if (NULL == LocalString->Buffer)
|
|
{
|
|
DebugLog((DEB_ERROR,"SspCopyClientString allocation failure!\n");
|
|
Status = STATUS_NO_MEMORY;
|
|
goto Cleanup;
|
|
}
|
|
LocalString->MaximumLength = sizeof(WCHAR);
|
|
*LocalString->Buffer = L"\0";
|
|
}
|
|
else
|
|
{
|
|
|
|
//
|
|
// Ensure no overflow against UNICODE_STRING, or desired string
|
|
//
|
|
SourceSize = (StringLength + 1) * CharacterSize;
|
|
if ((StringLength > MaxLength) || (SourceSize > 0xFFFF))
|
|
{
|
|
Status = STATUS_INVALID_PARAMETER;
|
|
DebugLog((DEB_WARN, "SspCopyClientString, String is too big for UNICODE_STRING\n"));
|
|
goto Cleanup;
|
|
}
|
|
|
|
TempString.Buffer = (LPSTR) SspAllocate(SourceSize);
|
|
if (NULL == TempString.Buffer)
|
|
{
|
|
DebugLog((DEB_ERROR,"SspCopyClientString allocation failure!\n");
|
|
Status = STATUS_NO_MEMORY;
|
|
goto Cleanup;
|
|
}
|
|
|
|
TempString.Length = (USHORT) (SourceSize - CharacterSize);
|
|
TempString.MaximumLength = (USHORT) SourceSize;
|
|
|
|
Status = LsaFunctions->CopyFromClientBuffer(
|
|
NULL,
|
|
SourceSize - CharacterSize,
|
|
TempString.Buffer,
|
|
StringPointer
|
|
);
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DebugLog((
|
|
DEB_ERROR,
|
|
"SspCopyClientString:LsaFn->CopyFromClientBuffer Failed - 0x%x\n",
|
|
Status));
|
|
goto Cleanup;
|
|
}
|
|
|
|
// We've put info into a STRING structure. Now do
|
|
// translation to UNICODE_STRING.
|
|
if (UnicodeString)
|
|
{
|
|
LocalString->Buffer = (LPWSTR) TemporaryString.Buffer;
|
|
LocalString->Length = TempString.Length;
|
|
LocalString->MaximumLength = TempString.MaximumLength;
|
|
}
|
|
else
|
|
{
|
|
NTSTATUS Status1;
|
|
Status1 = RtlOemStringToUnicodeString(
|
|
LocalString,
|
|
&TemporaryString,
|
|
TRUE
|
|
); // allocate destination
|
|
|
|
if (!NT_SUCCESS(Status1))
|
|
{
|
|
Status = STATUS_NO_MEMORY;
|
|
DebugLog((
|
|
DEB_ERROR,
|
|
"SspCopyClientString, Error from RtlOemStringToUnicodeString is 0x%lx\n",
|
|
Status
|
|
));
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
Cleanup:
|
|
|
|
if (TempString.Buffer != NULL)
|
|
{
|
|
//
|
|
// Free this if we failed and were doing unicode or if we weren't
|
|
// doing unicode
|
|
//
|
|
|
|
if ((UnicodeString && !NT_SUCCESS(Status)) || !UnicodeString)
|
|
{
|
|
NtLmFree(TemporaryString.Buffer);
|
|
}
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
//+-------------------------------------------------------------------------
|
|
//
|
|
// Function: SspAllocate
|
|
//
|
|
// Synopsis: Copies a string from the client and if necessary converts
|
|
// from ansi to unicode
|
|
//
|
|
// Effects: allocates output with either KerbAllocate (unicode)
|
|
// or RtlAnsiStringToUnicodeString
|
|
//
|
|
// Arguments: StringPointer - address of string in client process
|
|
// StringLength - Lenght (in characters) of string
|
|
// AnsiString - if TRUE, string is ansi
|
|
// LocalString - receives allocated string
|
|
//
|
|
// Requires:
|
|
//
|
|
// Returns:
|
|
//
|
|
// Notes:
|
|
//
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
|
|
NTSTATUS
|
|
SspCopyAuthorizationData(IN PVOID AuthorizationData,
|
|
IN OUT PBOOLEAN NullSession)
|
|
{
|
|
|
|
PSEC_WINNT_AUTH_IDENTITY_EXW pAuthIdentityEx = NULL;
|
|
BOOLEAN UnicodeString = TRUE;
|
|
|
|
// Init out parameters
|
|
*NullSession = FALSE;
|
|
|
|
|
|
if (NULL == AuthorizationData)
|
|
{
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
pAuthIdentity = (PSEC_WINNT_AUTH_IDENTITY_EXW)
|
|
SspAllocate(sizeof(SEC_WINNT_AUTH_IDENTITY_EXW));
|
|
|
|
if (NULL != pAuthIdentity)
|
|
{
|
|
Status = LsaFunctions->CopyFromClientBuffer(
|
|
NULL,
|
|
sizeof(SEC_WINNT_AUTH_IDENTITY),
|
|
pAuthIdentityEx,
|
|
AuthorizationData
|
|
);
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DebugLog((DEB_ERROR,"Fail: LsaFunctions->CopyFromClientBuffer is 0x%lx\n", Status));
|
|
goto Cleanup;
|
|
}
|
|
|
|
} else {
|
|
|
|
Status = STATUS_NO_MEMORY;
|
|
DebugLog((DEB_ERROR, "Fail: Alloc in SspCopyAuthData\n");
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// Do we have an EX version?
|
|
//
|
|
if (pAuthIdentityEx->Version == SEC_WINNT_AUTH_IDENTITY_VERSION)
|
|
{
|
|
Status = LsaFunctions->CopyFromClientBuffer(
|
|
NULL,
|
|
sizeof(SEC_WINNT_AUTH_IDENTITY_EXW),
|
|
pAuthIdentityEx,
|
|
AuthorizationData
|
|
);
|
|
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DebugLog((DEB_ERROR,"Fail: Error from LsaFunctions->CopyFromClientBuffer 0x%lx\n", Status));
|
|
goto Cleanup;
|
|
}
|
|
pAuthIdentity = (PSEC_WINNT_AUTH_IDENTITY) &pAuthIdentityEx->User;
|
|
CredSize = pAuthIdentityEx->Length;
|
|
Offset = FIELD_OFFSET(SEC_WINNT_AUTH_IDENTITY_EXW, User);
|
|
}
|
|
else
|
|
{
|
|
pAuthIdentity = (PSEC_WINNT_AUTH_IDENTITY_W) pAuthIdentityEx;
|
|
CredSize = sizeof(SEC_WINNT_AUTH_IDENTITY_W);
|
|
}
|
|
|
|
if ((pAuthIdentity->Flags & SEC_WINNT_AUTH_IDENTITY_ANSI) != 0)
|
|
{
|
|
DoUnicode = FALSE;
|
|
//
|
|
// Turn off the marshalled flag because we don't support marshalling
|
|
// with ansi.
|
|
//
|
|
|
|
pAuthIdentity->Flags &= ~SEC_WINNT_AUTH_IDENTITY_MARSHALLED;
|
|
}
|
|
else if ((pAuthIdentity->Flags & SEC_WINNT_AUTH_IDENTITY_UNICODE) == 0)
|
|
{
|
|
Status = SEC_E_INVALID_TOKEN;
|
|
SspPrint((SSP_CRITICAL,"SpAcquireCredentialsHandle, Error from pAuthIdentity->Flags is 0x%lx\n", pAuthIdentity->Flags));
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// For NTLM, we've got to verify that this is indeed a NULL session
|
|
//
|
|
if ((pAuthIdentity->UserLength == 0) &&
|
|
(pAuthIdentity->DomainLength == 0) &&
|
|
(pAuthIdentity->PasswordLength == 0) &&
|
|
(pAuthIdentity->User != NULL) &&
|
|
(pAuthIdentity->Domain != NULL) &&
|
|
(pAuthIdentity->Password != NULL))
|
|
{
|
|
*NullSession = TRUE;
|
|
}
|
|
|
|
//
|
|
// Copy over marshalled data
|
|
//
|
|
if((pAuthIdentity->Flags & SEC_WINNT_AUTH_IDENTITY_MARSHALLED) != 0 )
|
|
{
|
|
ULONG TmpCredentialSize;
|
|
ULONG_PTR EndOfCreds;
|
|
ULONG_PTR TmpUser;
|
|
ULONG_PTR TmpDomain;
|
|
ULONG_PTR TmpPassword;
|
|
|
|
if( pAuthIdentity->UserLength > UNLEN ||
|
|
pAuthIdentity->PasswordLength > PWLEN ||
|
|
pAuthIdentity->DomainLength > DNS_MAX_NAME_LENGTH ) {
|
|
|
|
SspPrint((SSP_CRITICAL,"Supplied credentials illegal length.\n"));
|
|
Status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// The callers can set the length of field to n chars, but they
|
|
// will really occupy n+1 chars (null-terminator).
|
|
//
|
|
|
|
TmpCredentialSize = CredSize +
|
|
( pAuthIdentity->UserLength +
|
|
pAuthIdentity->DomainLength +
|
|
pAuthIdentity->PasswordLength +
|
|
(((pAuthIdentity->User != NULL) ? 1 : 0) +
|
|
((pAuthIdentity->Domain != NULL) ? 1 : 0) +
|
|
((pAuthIdentity->Password != NULL) ? 1 : 0)) ) * sizeof(WCHAR);
|
|
|
|
EndOfCreds = (ULONG_PTR) AuthorizationData + TmpCredentialSize;
|
|
|
|
//
|
|
// Verify that all the offsets are valid and no overflow will happen
|
|
//
|
|
|
|
TmpUser = (ULONG_PTR) pAuthIdentity->User;
|
|
|
|
if ((TmpUser != NULL) &&
|
|
( (TmpUser < (ULONG_PTR) AuthorizationData) ||
|
|
(TmpUser > EndOfCreds) ||
|
|
((TmpUser + (pAuthIdentity->UserLength) * sizeof(WCHAR)) > EndOfCreds ) ||
|
|
((TmpUser + (pAuthIdentity->UserLength * sizeof(WCHAR))) < TmpUser)))
|
|
{
|
|
SspPrint((SSP_CRITICAL,"Username in supplied credentials has invalid pointer or length.\n"));
|
|
Status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
TmpDomain = (ULONG_PTR) pAuthIdentity->Domain;
|
|
|
|
if ((TmpDomain != NULL) &&
|
|
( (TmpDomain < (ULONG_PTR) AuthorizationData) ||
|
|
(TmpDomain > EndOfCreds) ||
|
|
((TmpDomain + (pAuthIdentity->DomainLength) * sizeof(WCHAR)) > EndOfCreds ) ||
|
|
((TmpDomain + (pAuthIdentity->DomainLength * sizeof(WCHAR))) < TmpDomain)))
|
|
{
|
|
SspPrint((SSP_CRITICAL,"Domainname in supplied credentials has invalid pointer or length.\n"));
|
|
Status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
TmpPassword = (ULONG_PTR) pAuthIdentity->Password;
|
|
|
|
if ((TmpPassword != NULL) &&
|
|
( (TmpPassword < (ULONG_PTR) AuthorizationData) ||
|
|
(TmpPassword > EndOfCreds) ||
|
|
((TmpPassword + (pAuthIdentity->PasswordLength) * sizeof(WCHAR)) > EndOfCreds ) ||
|
|
((TmpPassword + (pAuthIdentity->PasswordLength * sizeof(WCHAR))) < TmpPassword)))
|
|
{
|
|
SspPrint((SSP_CRITICAL,"Password in supplied credentials has invalid pointer or length.\n"));
|
|
Status = STATUS_INVALID_PARAMETER;
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// Allocate a chunk of memory for the credentials
|
|
//
|
|
|
|
TmpCredentials = (PSEC_WINNT_AUTH_IDENTITY_W) NtLmAllocate(TmpCredentialSize - Offset);
|
|
if (TmpCredentials == NULL)
|
|
{
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// Copy the credentials from the client
|
|
//
|
|
|
|
Status = LsaFunctions->CopyFromClientBuffer(
|
|
NULL,
|
|
TmpCredentialSize - Offset,
|
|
TmpCredentials,
|
|
(PUCHAR) AuthorizationData + Offset
|
|
);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
SspPrint((SSP_CRITICAL,"Failed to copy whole auth identity\n"));
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// Now convert all the offsets to pointers.
|
|
//
|
|
|
|
if (TmpCredentials->User != NULL)
|
|
{
|
|
USHORT cbUser;
|
|
|
|
TmpCredentials->User = (LPWSTR) RtlOffsetToPointer(
|
|
TmpCredentials->User,
|
|
(PUCHAR) TmpCredentials - (PUCHAR) AuthorizationData - Offset
|
|
);
|
|
|
|
ASSERT( (TmpCredentials->UserLength*sizeof(WCHAR)) <= 0xFFFF );
|
|
|
|
cbUser = (USHORT)(TmpCredentials->UserLength * sizeof(WCHAR));
|
|
UserName.Buffer = (PWSTR)NtLmAllocate( cbUser );
|
|
|
|
if (UserName.Buffer == NULL ) {
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
goto Cleanup;
|
|
}
|
|
|
|
CopyMemory( UserName.Buffer, TmpCredentials->User, cbUser );
|
|
UserName.Length = cbUser;
|
|
UserName.MaximumLength = cbUser;
|
|
}
|
|
|
|
if (TmpCredentials->Domain != NULL)
|
|
{
|
|
USHORT cbDomain;
|
|
|
|
TmpCredentials->Domain = (LPWSTR) RtlOffsetToPointer(
|
|
TmpCredentials->Domain,
|
|
(PUCHAR) TmpCredentials - (PUCHAR) AuthorizationData - Offset
|
|
);
|
|
|
|
ASSERT( (TmpCredentials->DomainLength*sizeof(WCHAR)) <= 0xFFFF );
|
|
cbDomain = (USHORT)(TmpCredentials->DomainLength * sizeof(WCHAR));
|
|
DomainName.Buffer = (PWSTR)NtLmAllocate( cbDomain );
|
|
|
|
if (DomainName.Buffer == NULL ) {
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
goto Cleanup;
|
|
}
|
|
|
|
CopyMemory( DomainName.Buffer, TmpCredentials->Domain, cbDomain );
|
|
DomainName.Length = cbDomain;
|
|
DomainName.MaximumLength = cbDomain;
|
|
}
|
|
|
|
if (TmpCredentials->Password != NULL)
|
|
{
|
|
USHORT cbPassword;
|
|
|
|
TmpCredentials->Password = (LPWSTR) RtlOffsetToPointer(
|
|
TmpCredentials->Password,
|
|
(PUCHAR) TmpCredentials - (PUCHAR) AuthorizationData - Offset
|
|
);
|
|
|
|
|
|
ASSERT( (TmpCredentials->PasswordLength*sizeof(WCHAR)) <= 0xFFFF );
|
|
cbPassword = (USHORT)(TmpCredentials->PasswordLength * sizeof(WCHAR));
|
|
Password.Buffer = (PWSTR)NtLmAllocate( cbPassword );
|
|
|
|
if (Password.Buffer == NULL ) {
|
|
ZeroMemory( TmpCredentials->Password, cbPassword );
|
|
Status = STATUS_INSUFFICIENT_RESOURCES;
|
|
goto Cleanup;
|
|
}
|
|
|
|
CopyMemory( Password.Buffer, TmpCredentials->Password, cbPassword );
|
|
Password.Length = cbPassword;
|
|
Password.MaximumLength = cbPassword;
|
|
|
|
ZeroMemory( TmpCredentials->Password, cbPassword );
|
|
}
|
|
|
|
|
|
}
|
|
//
|
|
// Data was *not* marshalled, copy strings individually
|
|
//
|
|
else
|
|
{
|
|
if (pAuthIdentity->Password != NULL)
|
|
{
|
|
Status = SspCopyClientString(
|
|
pAuthIdentity->Password,
|
|
pAuthIdentity->PasswordLength,
|
|
UnicodeString,
|
|
&Password
|
|
);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DebugLog((DEB_ERROR,"SpAcquireCredentialsHandle, Error from CopyClientString is 0x%lx\n", Status));
|
|
goto Cleanup;
|
|
}
|
|
|
|
}
|
|
|
|
if (pAuthIdentity->User != NULL)
|
|
{
|
|
Status = SspCopyClientString(
|
|
pAuthIdentity->User,
|
|
pAuthIdentity->UserLength,
|
|
UnicodeString,
|
|
&UserName
|
|
);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DebugLog((DEB_ERROR, "SpAcquireCredentialsHandle, Error from CopyClientString is 0x%lx\n", Status));
|
|
goto Cleanup;
|
|
}
|
|
|
|
}
|
|
|
|
if (pAuthIdentity->Domain != NULL)
|
|
{
|
|
Status = SspCopyClientString(
|
|
pAuthIdentity->Domain,
|
|
pAuthIdentity->DomainLength,
|
|
UnicodeString,
|
|
&DomainName
|
|
);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
DebugLog((DEB_ERROR, "SpAcquireCredentialsHandle, Error from CopyClientString is 0x%lx\n", Status));
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// Make sure that the domain name length is not greater
|
|
// than the allowed dns domain name
|
|
//
|
|
if (DomainName.Length > DNS_MAX_NAME_LENGTH * sizeof(WCHAR))
|
|
{
|
|
DebugLog((DEB_ERROR, "SpAcquireCredentialsHandle: Invalid supplied domain name %wZ\n",
|
|
&DomainName ));
|
|
Status = SEC_E_UNKNOWN_CREDENTIALS;
|
|
goto Cleanup;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|