Sunday, May 15, 2011

Windows Terminal Services (WTS) with VB.NET – what why how

I’m luckily have opportunity to works with Windows Terminal Services (WTS). The application that my team developed is running under Citrix machine, and there is a issue related with session information of each client ~ brief introduction.

Lets look more details about Windows Terminal Services (WTS).
Windows Terminal Services is one of Remote Access Technologies that allow IT administrators to perform administrative tasks; and IT users run programs on a remote machine as if they were working locally. This component provides the ability to host multiple, simultaneous client sessions on Windows Server machine. Through terminal emulation, its allows the same set of applications to run on diverse types of desktop hardware.
For organizations that looks flexibility to deploy applications and control desktop management costs, this architecture offers an important enhancement to the traditional two or three-tier client-server architecture based on servers and full-scale personal machine.

TerminalServicesArchitecture

Picture: Windows Terminal Services Architecture

Windows Terminal Services used the Remote Desktop Protocol (RDP) to interact with user. This protocol have following features and capabilities:

  • Encryption – RDP uses RSA security’s RC4 cipher ~ a stream cipher designed to efficiently encrypt small amounts of data. RC4 is designed for secure communications over networks.
  • Bandwidth Reduction – RDP supports various mechanisms to reduce the amount of data transmitted over a network connection. Mechanisms include data compression, persistent caching of bitmaps, and caching of glyphs and fragments in RAM
  • Roaming Disconnect – A user can manually disconnect from a remote desktop session without logging off. The user is automatically reconnected to their disconnected session when he or she logs back onto the system, either from the same device or a different device. When a user's session is unexpectedly terminated by a network or client failure, the user is disconnected but not logged off.
  • Clipboard Mapping – Users can delete, copy, and paste text and graphics between applications running on the local computer and those running in a remote desktop session, and between sessions.
  • Print Redirection – Applications running within a remote desktop session can print to a printer attached to the client device.
  • Virtual Channels – By using RDP virtual channel architecture, existing applications can be augmented and new applications can be developed to add features that require communications between the client device and an application running in a remote desktop session.
  • Remote Control – Computer support staff can view and control a remote desktop session. Sharing input and display graphics between two remote desktop sessions gives a support person the ability to diagnose and resolve problems remotely.
  • Network Load Balancing.
  • Smart Card Authentication through Remote Desktop Services.

TerminalServerInterationsWithRDP

Picture: Terminal Server Interactions with RDP

The user/ client initiates a connection to the Terminal Server through TCP port 3389. The Terminal Server RDP listener thread detects the session request and creates a new RDP stack instance to handle the new session request. The listener thread hands over the incoming session to the new RDP stack instance and continues listening on the TCP port for further connection attempts. Each RDP stack is created as the client sessions are connected to handle negotiation of session configuration details.

So, how I used it to help me to solve my team issue. There is one of Windows API called Remote Desktop Services API to implement the additional functionality, and dynamically link to the WtsApi32 library. There are several functions that I used to help me solve my team issue.

  • WTSEnumerateSessions – Retrieves a list of sessions on a specified Remote Desktop (RD) Session Host server.
  • WTSFreeMemory – Frees memory allocated by a Remote Desktop Services function.
  • WTSQuerySessionInformation – Retrieves session information for the specified session on the specified Remote Desktop (RD) Session Host server. It can be used to query session information on local and remote RD Session Host servers.
  • WTSOpenServer – Opens a handle to the specified Remote Desktop Session Host server.
  • WTSCloseServer – Closes an open handle to a Remote Desktop Session Host (RD Session Host) server.

And there are another functions from “Kernel32” library, that I used. There are:

  • GetCurrentProcessId – Retrieve the current of process Id.
  • WTSGetActiveConsoleSessionId – Retrieves the Remote Desktop Services session that is currently attached to the physical console. The physical console is the monitor, keyboard, and mouse. Note that it is not necessary that Remote Desktop Services be running for this function to succeed.
  • ProcessIdToSessionId – Retrieves the Remote Desktop Services session associated with a specified process.

 

   1:      Private Declare Function ProcessIdToSessionId Lib "Kernel32.dll" Alias "ProcessIdToSessionId" (ByVal processId As Int32, ByRef sessionId As Int32) As Boolean

   2:   

   3:      Private Declare Function WTSQuerySessionInformation Lib "WtsApi32.dll" Alias "WTSQuerySessionInformationW" (ByVal hServer As IntPtr, ByVal SessionId As Int32, ByVal WTSInfoClass As Int32, <MarshalAs(UnmanagedType.LPWStr)> ByRef ppBuffer As String, ByRef pCount As Int32) As Boolean

   4:   

   5:      Private Declare Function WTSQuerySessionInformation2 Lib "WtsApi32.dll" Alias "WTSQuerySessionInformationW" (ByVal hServer As IntPtr, ByVal SessionId As Int32, ByVal WTSInfoClass As Int32, ByRef ppBuffer As IntPtr, ByRef pCount As Int32) As Boolean

   6:   

   7:      Private Declare Function WTSEnumerateSessions Lib "WtsApi32.dll" Alias "WTSEnumerateSessions" (ByVal hServer As IntPtr, <MarshalAs(UnmanagedType.U4)> ByVal Reserved As Int32, <MarshalAs(UnmanagedType.U4)> ByVal Version As Int32, ByRef pSessionInfo As IntPtr, <MarshalAs(UnmanagedType.U4)> ByVal pCount As Int32) As Int32

   8:   

   9:      Private Declare Function WTSOpenServer Lib "WtsApi32.dll" Alias "WTSOpenServer" (ByVal pServerName As String) As IntPtr

  10:   

  11:      Private Declare Function WTSGetActiveConsoleSessionId Lib "Kernel32.dll" Alias "WTSGetActiveConsoleSessionId" () As Int32

  12:   

  13:      Private Declare Sub WTSCloseServer Lib "WtsApi32.dll" Alias "WTSCloseServer" (ByVal hServer As IntPtr)

  14:   

  15:      Private Declare Sub WTSFreeMemory Lib "WtsApi32.dll" Alias "WTSFreeMemory" (ByVal pMemory As IntPtr)

  16:   

  17:      Private Declare Function GetCurrentProcessId Lib "Kernel32.dll" Alias "GetCurrentProcessId" () As Int32





And I have enumeration of WTS information, called WTSInfoClass.


   1:      Private Enum WTSInfoClass As Integer
   2:          WTSInitialProgram
   3:          WTSApplicationName
   4:          WTSWorkingDirectory
   5:          WTSOEMId
   6:          WTSSessionId
   7:          WTSUserName
   8:          WTSWinStationName
   9:          WTSDomainName
  10:          WTSConnectState
  11:          WTSClientBuildNumber
  12:          WTSClientName
  13:          WTSClientDirectory
  14:          WTSClientProductId
  15:          WTSClientHardwareId
  16:          WTSClientAddress
  17:          WTSClientDisplay
  18:          WTSClientProtocolTyep
  19:          WTSIdleTime
  20:          WTSLogonTime
  21:          WTSIncomingBytes
  22:          WTSOutgoingBytes
  23:          WTSIncomingFrames
  24:          WTSOutgoingFrames
  25:      End Enum







And below is the main method to load session information of specified server name.


   1:  Public Function LoadSessionInfo(ByVal serverName As String) As SessionInfo
   2:          Dim ptrOpenedServer As IntPtr
   3:          Dim dictSessionIds As New Dictionary(Of String, Integer)
   4:   
   5:          Try
   6:              ptrOpenedServer = WTSOpenServer(serverName)
   7:              If ptrOpenedServer = vbNull Then
   8:                  Throw New Exception("Terminal Services not running on: " & serverName)
   9:              End If
  10:   
  11:              Dim retVal, count As Int32
  12:              Dim ptrSessionInfo As IntPtr = IntPtr.Zero
  13:              count = 0
  14:              Try
  15:                  retVal = WTSEnumerateSessions(ptrOpenedServer, 0, 1, ptrSessionInfo, count)
  16:                  If retVal <> 0 Then
  17:                      Dim sessionInfos() As WTS_SESSION_INFO = New WTS_SESSION_INFO(count) {}
  18:                      Dim ptrSession As IntPtr
  19:                      For i As Integer = 0 To count - 1
  20:                          ptrSession = ptrSessionInfo.ToInt32() + (i * Marshal.SizeOf(sessionInfos(i)))
  21:                          sessionInfos(i) = CType(Marshal.PtrToStructure(ptrSession, GetType(WTS_SESSION_INFO)), WTS_SESSION_INFO)
  22:                      Next
  23:   
  24:                      WTSFreeMemory(ptrSessionInfo)
  25:   
  26:                      Dim winStationName As String
  27:                      Dim tmpArr(sessionInfos.GetUpperBound(0)) As STR_SESSION_INFO
  28:                      For i = 0 To tmpArr.GetUpperBound(0)
  29:                          tmpArr(i).SessionID = sessionInfos(i).SessionId
  30:   
  31:                          winStationName = sessionInfos(i).pWinStationName
  32:                          tmpArr(i).StationName = winStationName
  33:   
  34:                          tmpArr(i).ConnectionState = sessionInfos(i).State.ToString()
  35:   
  36:                          If Not String.IsNullOrEmpty(winStationName) Then
  37:                              If Not dictSessionIds.ContainsKey(winStationName) Then
  38:                                  dictSessionIds.Add(winStationName, sessionInfos(i).SessionId)
  39:                              End If
  40:                          End If
  41:                      Next
  42:   
  43:                      ReDim sessionInfos(-1)
  44:                  Else
  45:                      Throw New Exception("No data returned")
  46:                  End If
  47:              Catch exInner As Exception
  48:                  Throw New Exception(exInner.Message & Environment.NewLine & Marshal.GetLastWin32Error)
  49:              End Try
  50:          Catch ex As Exception
  51:              Try
  52:                  WTSCloseServer(ptrOpenedServer)
  53:                  WTSFreeMemory(ptrOpenedServer)
  54:              Catch exWtsCloseServer As Exception
  55:              End Try
  56:   
  57:              Throw ex
  58:          End Try
  59:   
  60:          Dim result As New SessionInfo
  61:   
  62:          'Get ProcessId of TS Session that executed this TS Session
  63:          Dim activeProcess As Int32 = GetCurrentProcessId()
  64:          Dim activeSession As Int32 = 0
  65:          If Not ProcessIdToSessionId(activeProcess, activeSession) Then
  66:              'Nothing
  67:          End If
  68:   
  69:          Dim strBuffer As String
  70:          Dim ptrBuffer As IntPtr
  71:          Dim returnedLength As Int32
  72:   
  73:          'Member in this list will be ignore during generic query session iteration.
  74:          Dim ignoreQuerySession As New List(Of WTSInfoClass)
  75:   
  76:          'Member in this list will use function WTSQuerySessionInformation2
  77:          '    to get their information, instead WTSQuerySessionInformation.
  78:          '    By default, use WTSQuerySessionInformation is not contains in list.
  79:          Dim querySession2 As New List(Of WTSInfoClass)
  80:          querySession2.Add(WTSInfoClass.WTSClientAddress)
  81:   
  82:          'Get Windows Station Name, as a pre-requirement for some info,
  83:          '    likes: SessionId, ClientName, ClientAddress.
  84:          If WTSQuerySessionInformation(ptrOpenedServer, activeSession, WTSInfoClass.WTSWinStationName, strBuffer, returnedLength) Then
  85:              result.WinStationName = strBuffer
  86:              ignoreQuerySession.Add(WTSInfoClass.WTSWinStationName)
  87:   
  88:              'Skip ClientAddress and ClientName if this is a console session
  89:              If String.Equals(result.WinStationName, "Console") Then
  90:                  ignoreQuerySession.Add(WTSInfoClass.WTSClientAddress)
  91:                  ignoreQuerySession.Add(WTSInfoClass.WTSClientName)
  92:              End If
  93:          End If
  94:   
  95:          'Use stationName to get the correct sessionId from dictSessionId.
  96:          If dictSessionIds.ContainsKey(result.WinStationName) Then
  97:              Try
  98:                  result.SessionId = Integer.Parse(dictSessionIds(result.WinStationName))
  99:                  ignoreQuerySession.Add(WTSInfoClass.WTSSessionId)
 100:              Catch ex As Exception
 101:              End Try
 102:          End If
 103:   
 104:          Dim wtsInfoClassValues As Array = [Enum].GetValues(GetType(WTSInfoClass))
 105:          Dim wtsInfoClassItem As WTSInfoClass
 106:          Dim index As Integer = -1
 107:          Dim querySessionResult As Boolean
 108:          While index < wtsInfoClassValues.Length - 1
 109:              index += 1
 110:              wtsInfoClassItem = wtsInfoClassValues.GetValue(index)
 111:   
 112:              If ignoreQuerySession.Contains(wtsInfoClassItem) Then Continue While
 113:   
 114:              If querySession2.Contains(wtsInfoClassItem) Then
 115:                  querySessionResult = WTSQuerySessionInformation2(ptrOpenedServer, activeSession, wtsInfoClassItem, ptrBuffer, returnedLength)
 116:              Else
 117:                  querySessionResult = WTSQuerySessionInformation(ptrOpenedServer, activeSession, wtsInfoClassItem, strBuffer, returnedLength)
 118:              End If
 119:   
 120:              If querySessionResult Then
 121:                  Select Case wtsInfoClassItem
 122:   
 123:                      'TODO: Assign to proper place
 124:                      Case WTSInfoClass.WTSClientAddress
 125:                          Dim obj As New WTS_CLIENT_ADDRESS()
 126:                          obj = CType(Marshal.PtrToStructure(ptrBuffer, obj.GetType()), WTS_CLIENT_ADDRESS)
 127:                          result.Address(2) = obj.Address(2)
 128:                          result.Address(3) = obj.Address(3)
 129:                          result.Address(4) = obj.Address(4)
 130:                          result.Address(5) = obj.Address(5)
 131:   
 132:                  End Select
 133:              End If
 134:   
 135:              If ptrBuffer <> IntPtr.Zero Then
 136:                  WTSFreeMemory(ptrBuffer)
 137:                  ptrBuffer = IntPtr.Zero
 138:              End If
 139:              strBuffer = ""
 140:          End While
 141:   
 142:          WTSCloseServer(ptrOpenedServer)
 143:   
 144:          Return result
 145:      End Function



To supported above codes, I used 3 (three) structures: WTS_SESSION_INFO, STR_SESSION_INFO, and WTS_CLIENT_ADDRESS.


   1:     <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
   2:      Private Structure WTS_SESSION_INFO
   3:          Dim SessionId As Int32 'DWORD integer
   4:          Dim pWinStationName As String ' Integer LPTSTR - Pointer to a null-terminated string containing the name of the WinStation of this session
   5:          Dim State As WTSConnectStateClass
   6:      End Structure
   7:   
   8:      Private Structure STR_SESSION_INFO
   9:          Dim SessionId As Integer
  10:          Dim StationName As String
  11:          Dim ConnectionState As String
  12:      End Structure
  13:   
  14:      <StructLayout(LayoutKind.Sequential)> _
  15:      Private Structure WTS_CLIENT_ADDRESS
  16:          Public AddressFamily As Integer
  17:          <MarshalAs(UnmanagedType.ByValArray, SizeConst:=20)> _
  18:          Public Address As Byte()
  19:      End Structure
  20:   
  21:      Public Enum WTSConnectStateClass As Integer
  22:          WTSActive
  23:          WTSConnected
  24:          WTSConnectQuery
  25:          WTSShadow
  26:          WTSDisconnected
  27:          WTSIdle
  28:          WTSListen
  29:          WTSReset
  30:          WTSDown
  31:          WTSInit
  32:      End Enum





The return value of function LoadSessionInfo is SessionInfo.
SessionInfo is the plain/ model class that hold the WTS Information (I considered enumeration WTSInfoClass for those properties).


On line-123 “TODO: Assign to proper place”, this is the place to assign WTS information to our model class ~ SessionInfo. I put sample codes that assign the information of client address (IP).


I also have logic to check either application is running on remote terminal or local terminal ~ called IsRunningLocally.


   1:   Public Function IsRunningLocally() As Boolean
   2:          Dim ptrBuffer As IntPtr = IntPtr.Zero
   3:          Dim sessionId, currentSessionId, returnedLength As Integer
   4:   
   5:          Try
   6:              WTSQuerySessionInformation2(IntPtr.Zero, -1, WTSInfoClass.WTSSessionId, ptrBuffer, returnedLength)
   7:              sessionId = Marshal.ReadInt32(ptrBuffer, returnedLength)
   8:          Catch ex As Exception
   9:              sessionId = -1
  10:          Finally
  11:              WTSFreeMemory(ptrBuffer)
  12:              ptrBuffer = IntPtr.Zero
  13:          End Try
  14:   
  15:          Try
  16:              currentSessionId = WTSGetActiveConsoleSessionId()
  17:          Catch ex As Exception
  18:              currentSessionId = -1
  19:          End Try
  20:   
  21:          If currentSessionId <> sessionId Then Return False
  22:   
  23:          Return True
  24:      End Function



References


No comments:

Post a Comment