10

I'm trying to connect to remote powershell from C# .NET WinForms app. My goal is to create my own version of Microsoft PowerShell ISE. So i need a way to execute PowerShell Scripts from my app on Remote Machines. I've created couple of methods and tested it on local machine from my app. If I don't use WSManConnectionInfo and use using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace()) i can execute scripts locally as if it was true powershell (little scripts, usage of variables, output data using ft, fl, do a lot of other things I usually do with powershell. Problem starts when I add WSManConnectionInfo and point it to my Exchange Server instead of using local connection. It seems it's able to execute basic stuff like "get-mailbox" but as soon as i try to pipe things, use some scripting capabilities like $variables it breaks saying it's unsupported.

Similarly I have to disable powershell.AddCommand("out-string"); when not using it locally.

An unhandled exception of type 'System.Management.Automation.RemoteException' occurred in System.Management.Automation.dll.

Additional information: The term 'Out-String' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

The very same error doesn't appear if I don't force remote connection but simply do it locally. It seems like the SchemaUri is making it very strict to only execute basic commands. I saw other examples where people where using very direct information such us:

powershell.AddCommand("Get-Users");
powershell.AddParameter("ResultSize", count);

But with that approach I would have to define a lot of possible options and I don't want to go thru defining parameters and other stuff. I simply would like to load "script" and execute it just like in PowerShell window.. Here's an example of what I use now.

    public static WSManConnectionInfo PowerShellConnectionInformation(string serverUrl, PSCredential psCredentials)
    {
        var connectionInfo = new WSManConnectionInfo(new Uri(serverUrl), "http://schemas.microsoft.com/powershell/Microsoft.Exchange", psCredentials);
        //var connectionInfo = new WSManConnectionInfo(new Uri(serverUrl), "http://schemas.microsoft.com/powershell", psCredentials);
        connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
        connectionInfo.SkipCACheck = true;
        connectionInfo.SkipCNCheck = true;
        connectionInfo.SkipRevocationCheck = true;
        connectionInfo.MaximumConnectionRedirectionCount = 5;
        connectionInfo.OperationTimeout = 150000;
        return connectionInfo;
    }
    public static PSCredential SecurePassword(string login, string password)
    {
        SecureString ssLoginPassword = new SecureString();
        foreach (char x in password) { ssLoginPassword.AppendChar(x); }
        return new PSCredential(login, ssLoginPassword);
    }
    public static string RunScriptPs(WSManConnectionInfo connectionInfo, string scriptText)
    {
        StringBuilder stringBuilder = new StringBuilder();
        // Create a remote runspace using the connection information.
        //using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace())
        using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo))
        {
            // Establish the connection by calling the Open() method to open the runspace. 
            // The OpenTimeout value set previously will be applied while establishing 
            // the connection. Establishing a remote connection involves sending and 
            // receiving some data, so the OperationTimeout will also play a role in this process.
            remoteRunspace.Open();
            // Create a PowerShell object to run commands in the remote runspace.
            using (PowerShell powershell = PowerShell.Create())
            {
                powershell.Runspace = remoteRunspace;
                powershell.AddScript(scriptText);
                //powershell.AddCommand("out-string");
                powershell.Commands.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
                Collection<PSObject> results = powershell.Invoke();
            
                foreach (PSObject result in results) {
                        stringBuilder.AppendLine(result.ToString());
                }

            }
            // Close the connection. Call the Close() method to close the remote 
            // runspace. The Dispose() method (called by using primitive) will call 
            // the Close() method if it is not already called.
            remoteRunspace.Close();
        }

        // convert the script result into a single string
        return stringBuilder.ToString();
    }

Any advice on why this is happening and workaround how to get it to behave the same way? I've seen a lot of blogs like this but defining every simple command doesn't make sense to me. I also saw an option to create local connection and then execute remote connection within that but that's gotta be last resort since it relies on multiple other factors.

Community
  • 1
  • 1
MadBoy
  • 10,824
  • 24
  • 95
  • 156

3 Answers3

9

Check https://blogs.msdn.microsoft.com/akashb/2010/03/25/how-to-migrating-exchange-2007-powershell-managed-code-to-work-with-exchange-2010/:

The management experience given by Exchange 2010 through PowerShell has been moved all the way from Local to Remote. [...] Only exchange cmdlets will work in this remoting scenario, you will not be able to run most of the powershell cmdlets. [...] Yes, this does mean that you will not be able to run cmdlets like Where-Object and .PS1 scripts in the Remote Runspace.

Is that a limitation? I don’t think so. We can very easily get around it by create a new Session and Importing it.


So you'll need to do something like this:

PSCredential creds = new PSCredential(userName, securePassword);
System.Uri uri = new Uri("http://Exchange-Server/powershell?serializationLevel=Full");

Runspace runspace = RunspaceFactory.CreateRunspace();

PowerShell powershell = PowerShell.Create();
PSCommand command = new PSCommand();
command.AddCommand("New-PSSession");
command.AddParameter("ConfigurationName", "Microsoft.Exchange");
command.AddParameter("ConnectionUri", uri);
command.AddParameter("Credential", creds);
command.AddParameter("Authentication", "Default");
powershell.Commands = command;
runspace.Open(); powershell.Runspace = runspace;
Collection<PSSession> result = powershell.Invoke<PSSession>();

powershell = PowerShell.Create();
command = new PSCommand();
command.AddCommand("Set-Variable");
command.AddParameter("Name", "ra");
command.AddParameter("Value", result[0]);
powershell.Commands = command;
powershell.Runspace = runspace;
powershell.Invoke();

powershell = PowerShell.Create();
command = new PSCommand();
command.AddScript("Import-PSSession -Session $ra");
powershell.Commands = command;
powershell.Runspace = runspace;
powershell.Invoke();

# now you can use remote PS like it's local one
Community
  • 1
  • 1
Aziz Kabyshev
  • 790
  • 4
  • 8
  • It does seem legit. Will try it out tomorrow :-) Thank you. Hopefully that's the problem I'm having. – MadBoy Mar 30 '16 at 22:59
  • I also do hope this solves the problem. Best of luck with that. Please let me know – Aziz Kabyshev Mar 30 '16 at 23:07
  • @MadBoy hi there, how is it going? Maybe I should copy the code here to SO for future investigators, what do you think? – Aziz Kabyshev Apr 02 '16 at 19:56
  • Sorry I'm busy at work so couldn't test this. But yes. It makes sense. Blogs tend to die out. I do believe the answer makes sense to my question so I have to use the proposed workaround. – MadBoy Apr 03 '16 at 09:50
3

Starting from Exchange Server 2010 you need to use remote PowerShell session instead of adding exchange PowerShell snapin directly (due to using RBAC instead of ACL in exchange 2010). So you need to create new PowerShell session (using New-PSSession) and then import it (using Import-PSSession). You can use code like this to execute you PowerShell commands remote:

void ExecutePowerShellUsingRemotimg()
{
    RunspaceConfiguration runspaceConfig = RunspaceConfiguration.Create();
    PSSnapInException snapInException = null;

    Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfig);
    runspace.Open();

    Pipeline pipeline = runspace.CreatePipeline();


    string serverFqdn = "FQDN of you server";
    pipeline.Commands.AddScript(string.Format("$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://{0}/PowerShell/ -Authentication Kerberos", serverFqdn));
    pipeline.Commands.AddScript("Import-PSSession $Session");
    pipeline.Commands.AddScript("your PowerShell script text");
    pipeline.Commands.Add("Out-String");
    Collection<PSObject> results = pipeline.Invoke();
    runspace.Close();

    StringBuilder sb = new StringBuilder();

    if (pipeline.Error != null && pipeline.Error.Count > 0)
    {
        // Read errors
        succeeded = false;
        Collection<object> errors = pipeline.Error.ReadToEnd();
        foreach (object error in errors)
            sb.Append(error.ToString());
    }
    else
    {
        // Read output
        foreach (PSObject obj in results)
            sb.Append(obj.ToString());
    }

    runspace.Dispose();
    pipeline.Dispose();
}
SergeyIL
  • 575
  • 5
  • 11
  • Thank you. I've assigned the bounty to first correct answer. Your code is legit and very good and will be very helpful. – MadBoy Apr 05 '16 at 19:27
0

It looks like your running into double hop authentication problems, I've had the very same issues when trying to this.

After messing around I ended up installing the Exchange Powershell addins locally and used them to connect remotely to the Exchange server.

Rough example:

    RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
    PSSnapInInfo info = runspaceConfiguration.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapInException);                                    

    Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
    runspace.Open();

    using (PowerShell powershell = PowerShell.Create())
    {
        powershell.Runspace = runspace;

        string script = string.Format(@"Get-Mailbox -Server {0} -Identity {1}", serverName, identity);
        powershell.AddScript(script);

        powershell.Invoke();

       // Do something with the output

    }
ServerMonkey
  • 1,042
  • 3
  • 19
  • 42
  • Isn't it discouraged by Microsoft to use Exchange Addin in PowerShell (so I would expect the same being true for C# ?). And that would also make it nessecary for my app to have Exchange Tools installed locally which is not ideal. – MadBoy Mar 29 '16 at 07:14
  • Its not ideal but one of the few solutions that will work. I did this a few years ago and it worked but if I were to do it today I would consider building a small REST service or similar that you can run on the Exchange host, once again not ideal and has it's limitations but worth considering. NOTE: I assumed you looked into using the EWS and ruled that out, otherwise give it a look. – ServerMonkey Mar 29 '16 at 07:19
  • I don't want to install anything on Exchange Servers. I want to connect as i normally do with ISE. I don't think EWS has all that I need to do. I want standard powershell functionality. – MadBoy Mar 29 '16 at 09:55
  • I can't contribute to the C# side of things, but there is another way to get Exchange cmdlets without using the snap-in. You can poll using ADSI to find a CAS server and do $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://fqdngoeshere/powershell Import-PSSession $session | Out-Null to import all cmdlets available to a server running management tools. That's how I work on mailboxes anywhere I go without having to lug around 2GB of MT role files, and this method is taken directly from the MT role files. – Chris Kuperstein Apr 04 '16 at 21:43