--Original Post--
I am trying to manage (start/stop) a windows service on a remote machine using alternate credentials. I know that I can use the ServiceController class to manage a service using my current credentials:
Dim sc As New ServiceController(ServiceName, ComputerName)
but I want to use different credentials. The other classes I am using (DirectoryEntry and System.Management) both support using alternate credentials... Help would be greatly appreciated.
--Working Code (built based off accepted answer)--
I have to admit that I was sceptical it would work... but below is the code. I had to make a minor change to the code you suggested. Whenever I tried IPC$ it would return a 53 result code, even though I'm sure the share exists. So at the suggestion of another website I removed the share and just the computer name and this worked.
Imports System.Runtime.InteropServices
Imports System.Net
Imports System.IO
Imports System.ServiceProcess
Module Module1
Sub Main()
Dim Computername As String = "SomeComputer"
'Create connection to remote computer'
Using nc As New NetworkConnection("\\" + Computername, New NetworkCredential("Domain\User", "Password"))
Dim sc As New ServiceController("Windows Firewall/Internet Connection Sharing (ICS)", Computername)
'now we can start/stop/whatever we want here'
End Using
Console.ReadLine()
End Sub
Public Class NetworkConnection
Implements IDisposable
Private _networkName As String
Public Sub New(ByVal networkName As String, ByVal credentials As NetworkCredential)
_networkName = networkName
Dim netResource = New NetResource() With { _
.Scope = ResourceScope.GlobalNetwork, _
.ResourceType = ResourceType.Disk, _
.DisplayType = ResourceDisplaytype.Share, _
.RemoteName = networkName _
}
Dim result = WNetAddConnection2(netResource, credentials.Password, credentials.UserName, 0)
If result <> 0 Then
Throw New IOException("Error connecting to remote share", result)
End If
End Sub
Protected Overrides Sub Finalize()
Try
Dispose(False)
Finally
MyBase.Finalize()
End Try
End Sub
Public Sub Dispose() Implements System.IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Sub Dispose(ByVal disposing As Boolean)
WNetCancelConnection2(_networkName, 0, True)
End Sub
<DllImport("mpr.dll")> _
Private Shared Function WNetAddConnection2(ByVal netResource As NetResource, ByVal password As String, ByVal username As String, ByVal flags As Integer) As Integer
End Function
<DllImport("mpr.dll")> _
Private Shared Function WNetCancelConnection2(ByVal name As String, ByVal flags As Integer, ByVal force As Boolean) As Integer
End Function
End Class
<StructLayout(LayoutKind.Sequential)> _
Public Class NetResource
Public Scope As ResourceScope
Public ResourceType As ResourceType
Public DisplayType As ResourceDisplaytype
Public Usage As Integer
Public LocalName As String
Public RemoteName As String
Public Comment As String
Public Provider As String
End Class
Public Enum ResourceScope As Integer
Connected = 1
GlobalNetwork
Remembered
Recent
Context
End Enum
Public Enum ResourceType As Integer
Any = 0
Disk = 1
Print = 2
Reserved = 8
End Enum
Public Enum ResourceDisplaytype As Integer
Generic = &H0
Domain = &H1
Server = &H2
Share = &H3
File = &H4
Group = &H5
Network = &H6
Root = &H7
Shareadmin = &H8
Directory = &H9
Tree = &HA
Ndscontainer = &HB
End Enum
End Module
To make remote login you should use WNetAddConnection2 (see http://msdn.microsoft.com/en-us/library/aa385413.aspx) or NetUseAdd (see http://msdn.microsoft.com/en-us/library/aa370645.aspx) API. You can use \\RemoteComputer\IPC$ as the destination resource.
UPDATED based on the question from the comment: The explanation about IPC$ sessions can be long. Just the main information.
If you want to do something on a remote computer the first thing which will be done is the establishing a authenticated "connection" to the remote computer. The network login (remote login) on the remote computer will be done, which works quite other as a local login. The network logon session stay holding and if you have a connection to for example \\RemoteComputer\share1 and one other program on your computer try access for example \\RemoteComputer\share2, the same session will be used.
You can simulate the situation with net.exe. Just start cmd.exe and type
net use \\RemoteComputer\IPC$ /u:Domain\User password
or
net use \\RemoteComputer\IPC$ /u:RemoteComputer\LocalRemoteUser password
then you will have a connection to the destination computer. Then you can type \\RemoteComputer\AnyShare in Explorer and access file system under the user's Domain\User or RemoteComputer\LocalRemoteUser credential. To disconnect use
net use \\RemoteComputer\IPC /d
If you try to start/stop a service on the remote computer the same IPC session will be tried to established. If you have already such session with one of user's credentials it will be used. Functions WNetAddConnection2, NetUseAdd can be used as replacement of "net use". If you permanently want to access a remote computer with other user's credentials you can use CredWrite, CredWriteDomainCredentials or CredUIPromptForCredentials / CredUIPromptForWindowsCredentials. The Cred-function seems me not the best way for your case.
Related
I am making an app in VB.Net that copies many files and folders to the same directory and I wish to use windows explorer for that (so the user has GUI and I do not have to worry about showing any errors or compare files).
So, if I do this for each file/folder:
My.Computer.FileSystem.CopyDirectory(source_path, target_path, FileIO.UIOption.AllDialogs)
My.Computer.FileSystem.CopyFile(source_path, target_path, FileIO.UIOption.AllDialogs)
It works correctly and shows this window:
Which is fine, however, if I have many files and/or folders and I loop through them and call commands above, they launch a new copy window for each file/folder, instead of launching a single GUI that combines them all, like so:
Is it possible to combine multiple files/folders copy process into a single windows explorer copy window GUI?
Thanks to #Jimi, I got pointed in the direction of SHFileOperations, so i figured out how to do this. I made a small class to do this:
Imports System.Runtime.InteropServices
Public Class NativeCopy
Private Enum FO_Func As Short
FO_COPY = &H2
FO_DELETE = &H3
FO_MOVE = &H1
FO_RENAME = &H4
End Enum
Private Structure SHFILEOPSTRUCT
Public hwnd As IntPtr
Public wFunc As FO_Func
<MarshalAs(UnmanagedType.LPWStr)>
Public pFrom As String
<MarshalAs(UnmanagedType.LPWStr)>
Public pTo As String
Public fFlags As UShort
Public fAnyOperationsAborted As Boolean
Public hNameMappings As IntPtr
<MarshalAs(UnmanagedType.LPWStr)>
Public lpszProgressTitle As String
End Structure
<DllImport("shell32.dll", CharSet:=CharSet.Unicode)>
Private Shared Function SHFileOperation(
<[In]> ByRef lpFileOp As SHFILEOPSTRUCT) As Integer
End Function
Private Shared _ShFile As SHFILEOPSTRUCT
Public Shared Sub Copy(ByVal sSource As List(Of String), ByVal sTarget As String)
_ShFile.wFunc = FO_Func.FO_COPY
_ShFile.pFrom = String.Join(vbNullChar, sSource) + vbNullChar
_ShFile.pTo = sTarget
SHFileOperation(_ShFile)
End Sub
End Class
To copy files and/or folders is as simple as this:
Dim copy_items_paths As List(Of String)
Dim target_path As String
NativeCopy.Copy(copy_items_paths, target_path)
My issue is I cannot seem to put a delay on sending the item (have tried 2 minutes through to 2 days, no luck).
The mail itself sends immediately and without fail - it's just the delay that doesn't work? any help would be appreciated.
Note -
I have been using this as an example for most of my code.
I am using exchange 2010 SP2
The mail sends fine, just no delay
Public Class Mail
Private Const DEFERREDSENDTIMEFLAG As Integer = 16367
Public Shared ReadOnly Property EXCHANGESERVICEURL As String
Get
Return ConfigurationManager.AppSettings("EXCHANGESERVICEURL")
End Get
End Property
Public Shared ReadOnly Property DOMAINNAME As String
Get
Return ConfigurationManager.AppSettings("DOMAINNAME")
End Get
End Property
Public Shared ReadOnly Property EXCHANGEUSERNAME As String
Get
Return ConfigurationManager.AppSettings("EXCHANGEUSERNAME")
End Get
End Property
Public Shared ReadOnly Property EXCHANGEPASSWORD As String
Get
Return ConfigurationManager.AppSettings("EXCHANGEPASSWORD")
End Get
End Property
Public Shared ReadOnly Property EXCHANGEVERSION As ExchangeVersion
Get
Return CType(System.Enum.Parse(GetType(ExchangeVersion), ConfigurationManager.AppSettings("EXCHANGEVERSION")), ExchangeVersion)
End Get
End Property
Public Shared Sub SendMessage(ByVal fromAddress As String, ByVal toAddress() As String, ByVal ccAddress() As String, ByVal bccAddress() As String, ByVal subject As String, ByVal body As String, ByVal minutesDelay As Integer)
ServicePointManager.ServerCertificateValidationCallback = New RemoteCertificateValidationCallback(AddressOf ValidateCertificate)
Dim service As New ExchangeService(EXCHANGEVERSION)
service.Credentials = New WebCredentials(EXCHANGEUSERNAME, EXCHANGEPASSWORD, DOMAINNAME)
service.Url = New Uri(EXCHANGESERVICEURL)
Dim Message As New Microsoft.Exchange.WebServices.Data.EmailMessage(service)
'set delay send time
If minutesDelay > 0 Then
Dim sendTime As String = DateTime.Now.AddMinutes(minutesDelay).ToUniversalTime().ToString()
Dim PR_DEFERRED_SEND_TIME As New ExtendedPropertyDefinition(DEFERREDSENDTIMEFLAG, MapiPropertyType.SystemTime)
Message.SetExtendedProperty(PR_DEFERRED_SEND_TIME, sendTime)
End If
Message.From = fromAddress
If toAddress IsNot Nothing Then
For Each t As String In toAddress
Message.ToRecipients.Add(t)
Next
End If
If ccAddress IsNot Nothing Then
For Each t As String In ccAddress
Message.CcRecipients.Add(t)
Next
End If
If bccAddress IsNot Nothing Then
For Each t As String In bccAddress
Message.BccRecipients.Add(t)
Next
End If
Message.Subject = subject
Message.Body = body
Message.SendAndSaveCopy() 'save means make sure it's saved in the sent items folder
'message.Attachments
End Sub
Private Shared Function ValidateCertificate(sender As Object, certificate As X509Certificate, chain As X509Chain, sslPolicyErrors As SslPolicyErrors) As Boolean
Return True
End Function
End Class
I would suggest you used the typed variable rather then converting the Date-time to a string (like the example does) and then sending it. eg just use
Message.SetExtendedProperty(PR_DEFERRED_SEND_TIME, DateTime.Now.AddMinutes(minutesDelay).ToUniversalTime())
The library is designed to deal with typed variables but you can see the difference in the POST using a String vs Typed variable if you enable tracing eg
<t:ExtendedProperty>
<t:ExtendedFieldURI PropertyTag="16367" PropertyType="SystemTime"/>
<t:Value>2016-05-10T03:20:16.000</t:Value>
</t:ExtendedProperty>
vs
<t:ExtendedProperty>
<t:ExtendedFieldURI PropertyTag="16367" PropertyType="SystemTime"/>
<t:Value>2016-10-05T03:12:30.067Z</t:Value>
</t:ExtendedProperty>
You could also fix the string but using the typed varible makes more sense with that change you sample run fine for me.
TLDR: My console application runs if I click on it manually however it does not run when called by service while using StartProcess or Shell.
Could this be a permission issue?
Details:
I've written a small windows service application that checks a remote location folder for a product key and updates the local machine if the product key is different than what is entered. I thought this would be a simple project for my first foray into windows services.
The service runs using a timer every hour (for testing the interval is every 30 seconds). Originally the service would perform the updating but I ran into a more complicated issue accessing the UNC path (I would have to use Impersonation Classes).
So while testing the code in a console version of the application, I noticed it was able to access the network location without supplying credentials. So I rewrote the service so it calls the console application instead. But no matter how I write it I can't get the console application to launch from the service.
If anymore information is needed please feel free to ask!
Thank you for your time.
Damien hit the nail on the head, the issue is that when you run the console app, you are running it as you, which has access to the UNC. When the service runs your console app, it is running it as the user you tied to the service. You have a couple options:
1: Setup the service to run as a user with access to the UNC
2: Use Impersonation to connect as the user you want to.
Either way, running it from a console application instead of your service is useless.
I have included a class I created that makes impersonation easy:
Imports System.Security.Principal
Imports System.Runtime.InteropServices
''' <summary>
''' Used to temporarily impersonate another user
''' Must use a USING block or dispose of instance to undo impersonation
''' </summary>
''' <remarks></remarks>
Public Class ImpersonateFNS
Implements IDisposable
Declare Function LogonUserA Lib "advapi32.dll" (ByVal lpszUsername As String, ByVal lpszDomain As String, ByVal lpszPassword As String, _
ByVal dwLogonType As Integer, ByVal dwLogonProvider As Integer, ByRef phToken As IntPtr) As Integer
Private _impersonatedUser As WindowsImpersonationContext = Nothing
Private _loggedOn As Boolean = False
''' <summary>
''' Should call this in a USING block OR, make sure you dispose this object when done impersonating
''' </summary>
Public Sub New(ByVal Domain As String, ByVal UserName As String, ByVal Password As String)
Dim token As IntPtr = IntPtr.Zero
'If (LogonUserA(UserName, Domain, Password, 2, 0, token) <> 0) Then
If (LogonUserA(UserName, Domain, Password, 9, 0, token) <> 0) Then
_loggedOn = True
Dim newIdentity As WindowsIdentity = New WindowsIdentity(token)
_impersonatedUser = newIdentity.Impersonate()
Else
Dim ret As Integer = Marshal.GetLastWin32Error()
'Console.WriteLine("LogonUser failed with error code : {0}", ret)
'Throw New System.ComponentModel.Win32Exception(String.Format("LogonUser failed with error code : {0}", ret))
Throw New Security.SecurityException(String.Format("LogonUser failed with error code : {0}", ret))
End If
End Sub
Private ReadOnly Property LoggedOn As Boolean
Get
Return _loggedOn
End Get
End Property
#Region "IDisposable Support"
Private disposedValue As Boolean ' To detect redundant calls
' IDisposable
Protected Overridable Sub Dispose(disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
' TODO: dispose managed state (managed objects).
End If
If _impersonatedUser IsNot Nothing Then
_impersonatedUser.Undo()
End If
' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
' TODO: set large fields to null.
End If
Me.disposedValue = True
End Sub
' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
'Protected Overrides Sub Finalize()
' ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
' Dispose(False)
' MyBase.Finalize()
'End Sub
' This code added by Visual Basic to correctly implement the disposable pattern.
Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
#End Region
End Class
Create the class as shown above and then call is in a USING block so it will undo the impersonation when you are done doing your business. like so:
Using x as New ImpersonationFNF("MyDomain", "User","Password")
'Copy, read, whatever the stuff you need to do here
End Using
EDIT
I still can't assign the process the username of the session id that is associated with it.
This is the code i use to retrieve user details:
Public Sub GetUsers()
Using server As ITerminalServer = manager.GetRemoteServer(strHostName)
server.Open()
For Each session As ITerminalServicesSession In server.GetSessions()
If Not String.IsNullOrEmpty(session.UserName) Then
dictuser.Add(session.SessionId, New User(session.SessionId, session.UserName))
End If
Next
End Using
End Sub
my user class is defined simply as:
Public Class User
Private _SessionID As Integer
Private _UserName As String
Sub New(ByVal SessionID As Integer, ByVal UserName As String)
_SessionID = SessionID
_UserName = UserName
End Sub
Public ReadOnly Property SessionID As String
Get
Return _SessionID
End Get
End Property
Public ReadOnly Property UserName As String
Get
Return _UserName
End Get
End Property
End Class
i have created a function in my process class:
Public Sub AddUserInfo(ByVal UserName As String)
_UserName = UserName
End Sub
This replaces the process if a process with the same id is found in the dictionary and automatically adds a new process otherwise.
dictProcess(process.ProcessId) = process
EDIT (in response to your edited question):
I would change the name of the Processes class to Process, since it is not a collection, but is supposed to represent one process. You could change the constructor of the Process class to
Public Sub New(ByVal ProcessId As Integer, ByVal ProcessName As String, ByVal SessionId As Integer)
_ProcessId = ProcessId
_ProcessName = ProcessName
_SessionId = SessionId
End Sub
Then add a method
Public Sub AddWmiInfo (ByVal PageFileUsage As Integer, ByVal WorkingSetSize As Integer)
_PageFileUsage = PageFileUsage
_WorkingSetSize = WorkingSetSize
End Sub
Alternatively, you could also make these properties read/write, however you get a better encapsulation like this.
Add the basic process information to the dictionary using Cassia. Note that ProcessId is declared as Integer and thus the check Not String.IsNullOrEmpty(process.ProcessId) makes no sense.
For Each process As ITerminalServicesProcess In server.GetProcesses()
dictprocess.Add( _
process.ProcessId, _
New Process(process.ProcessId, process.ProcessName, process.SessionId))
Next
Finally, you would add the information from the WMI like this
Dim p As Process = Nothing
For Each wmiProcess In prowmi
If dictprocess.TryGetValue(wmiProcess.ProcessID, p) Then
p.AddWmiInfo(wmiProcess.PageFileUsage, wmiProcess.WorkingSetSize)
End If
Next
TryGetValue returns a process in the variable p. The second parameter of TryGetValue is a ByRef parameter.
EDIt #2:
Your loop should read
Dim p As Process = Nothing
For Each user As User In dictuser.Values
If dictprocess.TryGetValue(user.SessionID, p) Then
p.AddUserInfo(user.UserName)
End If
Next
However, if you are never accessing the user dictionary through the key (session id), then a list would be more appropriate than a dictionary.
In VB.NET, I am trying to retrieve what services are running on a TS using the following code:
Imports System.ServiceProcess
...
Dim dictservice As Generic.Dictionary(Of String, Services)
Public Sub GetService()
Dim localServices As ServiceController() = ServiceController.GetServices()
For Each service As ServiceController In localServices
dictservice.Add(service.DisplayName, New Services(service.DisplayName, service.ServiceName, service.Status.ToString))
Next
End Sub
My services class is as follows:
Class Services
Private _displayName As String
Private _serviceName As String
Private _serviceStatus As String
Sub New(ByVal DisplayName As String, ByVal ServiceName As Object, ByVal ServiceStatus As String)
_displayName = DisplayName
_serviceName = ServiceName
_serviceStatus = ServiceStatus
End Sub
Public Overrides Function ToString() As String
Return _serviceStatus
End Function
End Class
When i step through in debug mode it seems to being to populate the dictionary
display name: Application Experience
service name: AElookUpSVC
service status: Running (4)
When it tries to move onto the next item i get the following error:
Null reference exception was unhandled:
Object reference not set to an instance of an object.
I can't for the life of me work out where it is finding a null reference?
You need to initialize your dictionary with New:
Dim dictservice As New Generic.Dictionary(Of String, Services)
Public Sub GetService()
Dim localServices As ServiceController() = ServiceController.GetServices()
For Each service As ServiceController In localServices
dictservice.Add(service.DisplayName, New Services(service.DisplayName, service.ServiceName, service.Status.ToString))
Next
End Sub
Right now it's Nothing, hence the NullReferenceException.
The most likely problem appears to be that dictService is Nothing and hence you get a NullReferenceException. Where do you initialize / declare dictService and are you certain the initialization happens before this method?
If this is not the problem have you tried running this with the debugger attached? it should point you to the line that is failing.