mirror of https://github.com/tongzx/nt5src
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
802 lines
18 KiB
802 lines
18 KiB
/*++
|
|
|
|
Copyright (c) 1999-2000 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
policy.cpp
|
|
|
|
Abstract:
|
|
|
|
RDS Policy related function
|
|
|
|
Author:
|
|
|
|
HueiWang 5/2/2000
|
|
|
|
--*/
|
|
#include "stdafx.h"
|
|
#include "policy.h"
|
|
|
|
|
|
#ifndef __WIN9XBUILD__
|
|
|
|
extern "C" BOOLEAN RegDenyTSConnectionsPolicy();
|
|
|
|
typedef struct __RDSLevelShadowMap {
|
|
SHADOWCLASS shadowClass;
|
|
REMOTE_DESKTOP_SHARING_CLASS rdsLevel;
|
|
} RDSLevelShadowMap;
|
|
|
|
static const RDSLevelShadowMap ShadowMap[] = {
|
|
{ Shadow_Disable, NO_DESKTOP_SHARING }, // No RDS sharing
|
|
{ Shadow_EnableInputNotify, CONTROLDESKTOP_PERMISSION_REQUIRE }, // Interact with user permission
|
|
{ Shadow_EnableInputNoNotify, CONTROLDESKTOP_PERMISSION_NOT_REQUIRE }, // Interact without user permission
|
|
{ Shadow_EnableNoInputNotify, VIEWDESKTOP_PERMISSION_REQUIRE}, // View with user permission
|
|
{ Shadow_EnableNoInputNoNotify, VIEWDESKTOP_PERMISSION_NOT_REQUIRE } // View without user permission
|
|
};
|
|
|
|
|
|
DWORD
|
|
GetPolicyAllowGetHelpSetting(
|
|
HKEY hKey,
|
|
LPCTSTR pszKeyName,
|
|
LPCTSTR pszValueName,
|
|
IN DWORD* value
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Routine to query policy registry value.
|
|
|
|
Parameters:
|
|
|
|
hKey : Currently open registry key.
|
|
pszKeyName : Pointer to a null-terminated string containing
|
|
the name of the subkey to open.
|
|
pszValueName : Pointer to a null-terminated string containing
|
|
the name of the value to query
|
|
value : Pointer to DWORD to receive GetHelp policy setting.
|
|
|
|
Returns:
|
|
|
|
ERROR_SUCCESS or error code from RegOpenKeyEx().
|
|
|
|
--*/
|
|
{
|
|
DWORD dwStatus;
|
|
HKEY hPolicyKey = NULL;
|
|
DWORD dwType;
|
|
DWORD cbData;
|
|
|
|
//
|
|
// Open registry key for system policy
|
|
//
|
|
dwStatus = RegOpenKeyEx(
|
|
hKey,
|
|
pszKeyName,
|
|
0,
|
|
KEY_READ,
|
|
&hPolicyKey
|
|
);
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
// query value
|
|
cbData = 0;
|
|
dwType = 0;
|
|
dwStatus = RegQueryValueEx(
|
|
hPolicyKey,
|
|
pszValueName,
|
|
NULL,
|
|
&dwType,
|
|
NULL,
|
|
&cbData
|
|
);
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
if( REG_DWORD == dwType )
|
|
{
|
|
cbData = sizeof(DWORD);
|
|
|
|
// our registry value is REG_DWORD, if different type,
|
|
// assume not exist.
|
|
dwStatus = RegQueryValueEx(
|
|
hPolicyKey,
|
|
pszValueName,
|
|
NULL,
|
|
&dwType,
|
|
(LPBYTE)value,
|
|
&cbData
|
|
);
|
|
|
|
ASSERT( ERROR_SUCCESS == dwStatus );
|
|
}
|
|
else
|
|
{
|
|
// bad registry key type, assume
|
|
// key does not exist.
|
|
dwStatus = ERROR_FILE_NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
RegCloseKey( hPolicyKey );
|
|
}
|
|
|
|
return dwStatus;
|
|
}
|
|
|
|
|
|
SHADOWCLASS
|
|
MapRDSLevelToTSShadowSetting(
|
|
IN REMOTE_DESKTOP_SHARING_CLASS RDSLevel
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Convert TS Shadow settings to our RDS sharing level.
|
|
|
|
Parameter:
|
|
|
|
TSShadowClass : TS Shadow setting.
|
|
|
|
Returns:
|
|
|
|
REMOTE_DESKTOP_SHARING_CLASS
|
|
|
|
--*/
|
|
{
|
|
SHADOWCLASS shadowClass;
|
|
|
|
for( int i=0; i < sizeof(ShadowMap)/sizeof(ShadowMap[0]); i++)
|
|
{
|
|
if( ShadowMap[i].rdsLevel == RDSLevel )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( i < sizeof(ShadowMap)/sizeof(ShadowMap[0]) )
|
|
{
|
|
shadowClass = ShadowMap[i].shadowClass;
|
|
}
|
|
else
|
|
{
|
|
MYASSERT(FALSE);
|
|
shadowClass = Shadow_Disable;
|
|
}
|
|
|
|
return shadowClass;
|
|
}
|
|
|
|
|
|
REMOTE_DESKTOP_SHARING_CLASS
|
|
MapTSShadowSettingToRDSLevel(
|
|
SHADOWCLASS TSShadowClass
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Convert TS Shadow settings to our RDS sharing level.
|
|
|
|
Parameter:
|
|
|
|
TSShadowClass : TS Shadow setting.
|
|
|
|
Returns:
|
|
|
|
REMOTE_DESKTOP_SHARING_CLASS
|
|
|
|
--*/
|
|
{
|
|
REMOTE_DESKTOP_SHARING_CLASS level;
|
|
|
|
for( int i=0; i < sizeof(ShadowMap)/sizeof(ShadowMap[0]); i++)
|
|
{
|
|
if( ShadowMap[i].shadowClass == TSShadowClass )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( i < sizeof(ShadowMap)/sizeof(ShadowMap[0]) )
|
|
{
|
|
level = ShadowMap[i].rdsLevel;
|
|
}
|
|
else
|
|
{
|
|
MYASSERT(FALSE);
|
|
level = NO_DESKTOP_SHARING;
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
|
|
DWORD
|
|
MapSessionIdToWinStationName(
|
|
IN ULONG ulSessionID,
|
|
OUT PWINSTATIONNAME pWinstationName
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Find out TS winstation name for the session specified.
|
|
|
|
Parameters:
|
|
|
|
ulSessionID : TS Session ID to query.
|
|
pWinstationName : Pointer to WINSTATIONNAME to receive
|
|
name of WinStation for Session ID specified.
|
|
|
|
Returns:
|
|
|
|
ERROR_SUCCESS or Error code.
|
|
|
|
-*/
|
|
{
|
|
BOOL bSuccess;
|
|
DWORD dwStatus;
|
|
LPTSTR pBuffer = NULL;
|
|
DWORD bytesReturned;
|
|
|
|
bSuccess = WTSQuerySessionInformation(
|
|
WTS_CURRENT_SERVER,
|
|
ulSessionID,
|
|
WTSWinStationName,
|
|
&pBuffer,
|
|
&bytesReturned
|
|
);
|
|
|
|
if( TRUE == bSuccess )
|
|
{
|
|
LPTSTR pszChr;
|
|
|
|
//
|
|
// WINSTATIONNAME returned from WTSQuerySessionInformation
|
|
// has '#...' appended to it, we can't use it to query
|
|
// WINSTATION configuration as RegApi look for exact WINSTATION
|
|
// name in registry and so it will return default value.
|
|
//
|
|
pszChr = _tcschr( pBuffer, _TEXT('#') );
|
|
if( NULL != pszChr )
|
|
{
|
|
*pszChr = _TEXT('\0');
|
|
}
|
|
|
|
ZeroMemory( pWinstationName, sizeof( WINSTATIONNAME ) );
|
|
CopyMemory( pWinstationName, pBuffer, bytesReturned );
|
|
dwStatus = ERROR_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
dwStatus = GetLastError();
|
|
}
|
|
|
|
if( NULL != pBuffer )
|
|
{
|
|
WTSFreeMemory( pBuffer );
|
|
}
|
|
|
|
return dwStatus;
|
|
}
|
|
|
|
DWORD
|
|
GetWinStationConfig(
|
|
IN ULONG ulSessionId,
|
|
OUT WINSTATIONCONFIG2* pConfig
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Retrieve WINSTATIONCONFIG2 for session specified.
|
|
|
|
Parameters:
|
|
|
|
ulSessionId : TS Session to query.
|
|
pConfig : Pointer to WINSTATIONCONIF2 to receive result.
|
|
|
|
Returns:
|
|
|
|
ERROR_SUCCESS or error code.
|
|
|
|
--*/
|
|
{
|
|
WINSTATIONNAME WinStationName;
|
|
DWORD dwStatus;
|
|
ULONG length = 0;
|
|
|
|
dwStatus = MapSessionIdToWinStationName(
|
|
ulSessionId,
|
|
WinStationName
|
|
);
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
dwStatus = RegWinStationQuery(
|
|
NULL,
|
|
WinStationName,
|
|
pConfig,
|
|
sizeof(WINSTATIONCONFIG2),
|
|
&length
|
|
);
|
|
}
|
|
|
|
return dwStatus;
|
|
}
|
|
|
|
#endif
|
|
|
|
BOOL
|
|
IsUserAllowToGetHelp(
|
|
IN ULONG ulSessionId,
|
|
IN LPCTSTR pszUserSid
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Determine if caller can 'GetHelp'
|
|
|
|
Parameters:
|
|
|
|
ulSessionId : User's TS logon ID.
|
|
pszUserSid : User's SID in textual form.
|
|
|
|
Returns:
|
|
|
|
TRUE/FALSE
|
|
|
|
Note:
|
|
|
|
Must have impersonate user first.
|
|
|
|
--*/
|
|
{
|
|
BOOL bAllow;
|
|
DWORD dwStatus;
|
|
DWORD dwAllow;
|
|
LPTSTR pszUserHive = NULL;
|
|
|
|
MYASSERT( NULL != pszUserSid );
|
|
|
|
//
|
|
// Must be able to GetHelp from machine
|
|
//
|
|
bAllow = TSIsMachinePolicyAllowHelp();
|
|
if( TRUE == bAllow )
|
|
{
|
|
pszUserHive = (LPTSTR)LocalAlloc(
|
|
LPTR,
|
|
sizeof(TCHAR) * (lstrlen(pszUserSid) + lstrlen(RDS_GROUPPOLICY_SUBTREE) + 2 )
|
|
);
|
|
|
|
lstrcpy( pszUserHive, pszUserSid );
|
|
lstrcat( pszUserHive, _TEXT("\\") );
|
|
lstrcat( pszUserHive, RDS_GROUPPOLICY_SUBTREE );
|
|
|
|
//
|
|
// Query user level AllowGetHelp setting.
|
|
dwStatus = GetPolicyAllowGetHelpSetting(
|
|
HKEY_USERS,
|
|
pszUserHive,
|
|
RDS_ALLOWGETHELP_VALUENAME,
|
|
&dwAllow
|
|
);
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
bAllow = (POLICY_ENABLE == dwAllow);
|
|
}
|
|
else
|
|
{
|
|
// no configuration for this user, assume GetHelp
|
|
// is enabled.
|
|
bAllow = TRUE;
|
|
}
|
|
}
|
|
|
|
if( NULL != pszUserHive )
|
|
{
|
|
LocalFree( pszUserHive );
|
|
}
|
|
|
|
return bAllow;
|
|
}
|
|
|
|
DWORD
|
|
GetSystemRDSLevel(
|
|
IN ULONG ulSessionId,
|
|
OUT REMOTE_DESKTOP_SHARING_CLASS* pSharingLevel
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Retrieve policy setting for remote desktop sharing level.
|
|
|
|
Parameters:
|
|
|
|
ulSessionId : TS session ID, unuse if TS group policy is set.
|
|
pSharingLever : Pointer to REMOTE_DESKTOP_SHARING_CLASS to receive
|
|
machine RDS level.
|
|
|
|
Returns:
|
|
|
|
REMOTE_DESKTOP_SHARING_CLASS
|
|
|
|
--*/
|
|
{
|
|
DWORD dwStatus;
|
|
|
|
#ifndef __WIN9XBUILD__
|
|
|
|
WINSTATIONCONFIG2 WSConfig;
|
|
ULONG length = 0;
|
|
|
|
//
|
|
// TS Group Policy does not have machine level shadow
|
|
// setting, only TSCC has this setting, and TSCC setting
|
|
// is based on WINSTATION not entire machine.
|
|
//
|
|
// We can't query TS since TS already merger all policy
|
|
// setting into USERCONFIG which might not be correct
|
|
//
|
|
|
|
dwStatus = GetWinStationConfig(
|
|
ulSessionId,
|
|
&WSConfig
|
|
);
|
|
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
if( TRUE == WSConfig.Config.User.fInheritShadow )
|
|
{
|
|
// Shadow config is inherite from user properies
|
|
// so we query user level setting.
|
|
dwStatus = GetUserRDSLevel( ulSessionId, pSharingLevel );
|
|
}
|
|
else
|
|
{
|
|
*pSharingLevel = MapTSShadowSettingToRDSLevel( WSConfig.Config.User.Shadow );
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
// TODO - revisit this for Win9x Build
|
|
MYASSERT(FALSE);
|
|
*pSharingLevel = CONTROLDESKTOP_PERMISSION_REQUIRE;
|
|
dwStatus = ERROR_SUCCESS;
|
|
|
|
#endif
|
|
|
|
return dwStatus;
|
|
}
|
|
|
|
|
|
DWORD
|
|
GetUserRDSLevel(
|
|
IN ULONG ulSessionId,
|
|
OUT REMOTE_DESKTOP_SHARING_CLASS* pLevel
|
|
)
|
|
/*++
|
|
|
|
same as GetSystemRDSLevel() except it retrieve currently logon user's
|
|
RDS level.
|
|
|
|
--*/
|
|
{
|
|
DWORD dwStatus;
|
|
|
|
#ifndef __WIN9XBUILD__
|
|
|
|
BOOL bSuccess;
|
|
WINSTATIONCONFIG WSConfig;
|
|
DWORD dwByteReturned;
|
|
|
|
memset( &WSConfig, 0, sizeof(WSConfig) );
|
|
|
|
// Here we call WInStationQueryInformation() since WTSAPI require
|
|
// few calls to get the same result
|
|
bSuccess = WinStationQueryInformation(
|
|
WTS_CURRENT_SERVER,
|
|
ulSessionId,
|
|
WinStationConfiguration,
|
|
&WSConfig,
|
|
sizeof( WSConfig ),
|
|
&dwByteReturned
|
|
);
|
|
|
|
|
|
if( TRUE == bSuccess )
|
|
{
|
|
dwStatus = ERROR_SUCCESS;
|
|
*pLevel = MapTSShadowSettingToRDSLevel( WSConfig.User.Shadow );
|
|
}
|
|
else
|
|
{
|
|
dwStatus = GetLastError();
|
|
}
|
|
|
|
#else
|
|
|
|
// TODO - revisit this for Win9x Build
|
|
MYASSERT(FALSE);
|
|
*pLevel = CONTROLDESKTOP_PERMISSION_REQUIRE;
|
|
dwStatus = ERROR_SUCCESS;
|
|
|
|
#endif
|
|
|
|
return dwStatus;
|
|
}
|
|
|
|
|
|
DWORD
|
|
ConfigSystemGetHelp(
|
|
BOOL bEnable
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Enable/disable 'GetHelp' on local machine.
|
|
|
|
Parameters:
|
|
|
|
bEnable : TRUE to enable, FALSE otherwise.
|
|
|
|
Returns:
|
|
|
|
ERROR_SUCCESS or error code.
|
|
|
|
--*/
|
|
{
|
|
DWORD dwStatus = ERROR_SUCCESS;
|
|
BOOL bMember;
|
|
HKEY hKey = NULL;
|
|
DWORD dwValue;
|
|
BOOL bAllowHelp;
|
|
|
|
#ifndef __WIN9XBUILD__
|
|
|
|
dwStatus = IsUserAdmin( &bMember );
|
|
if( ERROR_SUCCESS != dwStatus )
|
|
{
|
|
goto CLEANUPANDEXIT;
|
|
}
|
|
|
|
if( FALSE == bMember )
|
|
{
|
|
dwStatus = ERROR_ACCESS_DENIED;
|
|
goto CLEANUPANDEXIT;
|
|
}
|
|
|
|
#endif
|
|
|
|
// We only check if Group Policy has this setting and no
|
|
// checking on TSCC deny connection setting since
|
|
// TSCC set WINSTATION deny connection not entire machine
|
|
// and we can't be sure which connection that connection
|
|
// is denied, so it is still possible that GetHelp is enabled
|
|
// but WINSTATION's deny connection is still set to TRUE
|
|
// in this case, no help is available even this funtion
|
|
// return SUCCEEDED.
|
|
|
|
|
|
// verify no group policy on this
|
|
dwStatus = GetPolicyAllowGetHelpSetting(
|
|
HKEY_LOCAL_MACHINE,
|
|
RDS_GROUPPOLICY_SUBTREE,
|
|
RDS_ALLOWGETHELP_VALUENAME,
|
|
&dwValue
|
|
);
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
dwStatus = ERROR_ACCESS_DENIED;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Write to registry
|
|
//
|
|
dwStatus = RegCreateKeyEx(
|
|
HKEY_LOCAL_MACHINE,
|
|
RDS_MACHINEPOLICY_SUBTREE,
|
|
0,
|
|
NULL,
|
|
REG_OPTION_NON_VOLATILE,
|
|
KEY_ALL_ACCESS,
|
|
NULL,
|
|
&hKey,
|
|
NULL
|
|
);
|
|
|
|
if( ERROR_SUCCESS == dwStatus )
|
|
{
|
|
dwValue = (bEnable) ? POLICY_ENABLE : POLICY_DISABLE;
|
|
|
|
dwStatus = RegSetValueEx(
|
|
hKey,
|
|
RDS_ALLOWGETHELP_VALUENAME,
|
|
0,
|
|
REG_DWORD,
|
|
(LPBYTE) &dwValue,
|
|
sizeof(DWORD)
|
|
);
|
|
}
|
|
|
|
MYASSERT( ERROR_SUCCESS == dwStatus );
|
|
}
|
|
|
|
#ifndef __WIN9XBUILD__
|
|
|
|
CLEANUPANDEXIT:
|
|
|
|
#endif
|
|
|
|
if( NULL != hKey )
|
|
{
|
|
RegCloseKey(hKey);
|
|
}
|
|
|
|
return dwStatus;
|
|
}
|
|
|
|
DWORD
|
|
ConfigSystemRDSLevel(
|
|
IN REMOTE_DESKTOP_SHARING_CLASS level
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This routine set machine wide RDS level.
|
|
|
|
Parameters:
|
|
|
|
level : new RDS level.
|
|
|
|
Returns:
|
|
|
|
ERROR_SUCCESS or error code.
|
|
--*/
|
|
{
|
|
//
|
|
// TODO - revisit this, on Win2K and Whilster,
|
|
// group policy override this setting via User Properties
|
|
// or if group policy is not set, TSCC can override this.
|
|
// and since a user account always has some default value
|
|
// for this, so we have no way to determine whether this is
|
|
// a default or else, so we simple return ERROR for Win2K
|
|
// and Whilster platform.
|
|
//
|
|
|
|
//
|
|
// TODO : For legacy platform, Win9x/NT40/TS40, we need to figure
|
|
// out how/where poledit put this setting, for now,
|
|
// just return error.
|
|
//
|
|
DWORD dwStatus = ERROR_INVALID_FUNCTION;
|
|
return dwStatus;
|
|
}
|
|
|
|
|
|
DWORD
|
|
ConfigUserSessionRDSLevel(
|
|
IN ULONG ulSessionId,
|
|
IN REMOTE_DESKTOP_SHARING_CLASS level
|
|
)
|
|
/*++
|
|
|
|
--*/
|
|
{
|
|
#ifndef __WIN9XBUILD__
|
|
|
|
WINSTATIONCONFIG winstationConfig;
|
|
SHADOWCLASS shadowClass = MapRDSLevelToTSShadowSetting( level );
|
|
BOOL bSuccess;
|
|
DWORD dwLength;
|
|
DWORD dwStatus;
|
|
|
|
memset( &winstationConfig, 0, sizeof(winstationConfig) );
|
|
|
|
bSuccess = WinStationQueryInformation(
|
|
WTS_CURRENT_SERVER,
|
|
ulSessionId,
|
|
WinStationConfiguration,
|
|
&winstationConfig,
|
|
sizeof(winstationConfig),
|
|
&dwLength
|
|
);
|
|
|
|
if( TRUE == bSuccess )
|
|
{
|
|
winstationConfig.User.Shadow = shadowClass;
|
|
|
|
bSuccess = WinStationSetInformation(
|
|
WTS_CURRENT_SERVER,
|
|
ulSessionId,
|
|
WinStationConfiguration,
|
|
&winstationConfig,
|
|
sizeof(winstationConfig)
|
|
);
|
|
}
|
|
|
|
if( TRUE == bSuccess )
|
|
{
|
|
dwStatus = ERROR_SUCCESS;
|
|
}
|
|
else
|
|
{
|
|
dwStatus = GetLastError();
|
|
}
|
|
|
|
return dwStatus;
|
|
|
|
#else
|
|
|
|
return ERROR_SUCCESS;
|
|
|
|
#endif
|
|
}
|
|
|
|
#if 0
|
|
|
|
HRESULT
|
|
PolicyGetAllowUnSolicitedHelp(
|
|
BOOL* bAllow
|
|
)
|
|
/*++
|
|
|
|
--*/
|
|
{
|
|
HRESULT hRes;
|
|
CComPtr<IRARegSetting> IRegSetting;
|
|
|
|
hRes = IRegSetting.CoCreateInstance( CLSID_RARegSetting );
|
|
if( SUCCEEDED(hRes) )
|
|
{
|
|
hRes = IRegSetting->get_AllowUnSolicited(bAllow);
|
|
}
|
|
|
|
MYASSERT( SUCCEEDED(hRes) );
|
|
|
|
return hRes;
|
|
}
|
|
|
|
#endif
|
|
|
|
HRESULT
|
|
PolicyGetMaxTicketExpiry(
|
|
LONG* value
|
|
)
|
|
/*++
|
|
|
|
--*/
|
|
{
|
|
HRESULT hRes;
|
|
CComPtr<IRARegSetting> IRegSetting;
|
|
|
|
hRes = IRegSetting.CoCreateInstance( CLSID_RARegSetting );
|
|
if( SUCCEEDED(hRes) )
|
|
{
|
|
hRes = IRegSetting->get_MaxTicketExpiry(value);
|
|
}
|
|
|
|
MYASSERT( SUCCEEDED(hRes) );
|
|
return hRes;
|
|
}
|
|
|
|
|
|
|
|
|
|
|