diff --git a/LANCommander.SDK/Helpers/ScriptHelper.cs b/LANCommander.SDK/Helpers/ScriptHelper.cs index 3232e57..dd27f7e 100644 --- a/LANCommander.SDK/Helpers/ScriptHelper.cs +++ b/LANCommander.SDK/Helpers/ScriptHelper.cs @@ -14,15 +14,24 @@ namespace LANCommander.SDK.Helpers public static readonly ILogger Logger; public static string SaveTempScript(Script script) + { + var tempPath = SaveTempScript(script.Contents); + + Logger?.LogTrace("Wrote script {Script} to {Destination}", script.Name, tempPath); + + return tempPath; + } + + public static string SaveTempScript(string contents) { var tempPath = Path.GetTempFileName(); // PowerShell will only run scripts with the .ps1 file extension File.Move(tempPath, tempPath + ".ps1"); - Logger?.LogTrace("Writing script {Script} to {Destination}", script.Name, tempPath); + tempPath = tempPath + ".ps1"; - File.WriteAllText(tempPath, script.Contents); + File.WriteAllText(tempPath, contents); return tempPath; } diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index e653767..2e097cf 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -6,6 +6,7 @@ + diff --git a/LANCommander.SDK/PowerShell/PowerShellArgument.cs b/LANCommander.SDK/PowerShell/PowerShellArgument.cs new file mode 100644 index 0000000..173e861 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellArgument.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public class PowerShellArgument + { + public string Name { get; set; } + public object Value { get; set; } + public Type Type { get; set; } + + public PowerShellArgument(string name, object value, Type type) + { + Name = name; + Value = value; + Type = type; + } + } +} diff --git a/LANCommander.SDK/PowerShell/PowerShellFactory.cs b/LANCommander.SDK/PowerShell/PowerShellFactory.cs new file mode 100644 index 0000000..ace2d78 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellFactory.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public static class PowerShellFactory + { + public static PowerShellScript RunScript() + { + return new PowerShellScript(); + } + } +} diff --git a/LANCommander.SDK/PowerShell/PowerShellScript.cs b/LANCommander.SDK/PowerShell/PowerShellScript.cs new file mode 100644 index 0000000..6f300f9 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellScript.cs @@ -0,0 +1,169 @@ +using LANCommander.SDK.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public class PowerShellScript + { + private string Contents { get; set; } = ""; + private string WorkingDirectory { get; set; } = ""; + private bool AsAdmin { get; set; } = false; + private bool ShellExecute { get; set; } = false; + private bool IgnoreWow64 { get; set; } = false; + private ICollection Variables { get; set; } + private Dictionary Arguments { get; set; } + private Process Process { get; set; } + + public PowerShellScript() + { + Variables = new List(); + Arguments = new Dictionary(); + Process = new Process(); + + Process.StartInfo.FileName = "powershell.exe"; + Process.StartInfo.RedirectStandardOutput = false; + + AddArgument("ExecutionPolicy", "Unrestricted"); + } + + public PowerShellScript UseFile(string path) + { + Contents = File.ReadAllText(path); + + return this; + } + + public PowerShellScript UseInline(string contents) + { + Contents = contents; + + return this; + } + + public PowerShellScript UseWorkingDirectory(string path) + { + WorkingDirectory = path; + + return this; + } + + public PowerShellScript UseShellExecute() + { + ShellExecute = true; + + return this; + } + + public PowerShellScript AddVariable(string name, T value) + { + Variables.Add(new PowerShellVariable(name, value, typeof(T))); + + return this; + } + + public PowerShellScript AddArgument(string name, T value) + { + Arguments.Add(name, $"\"{value}\""); + + return this; + } + + public PowerShellScript AddArgument(string name, int value) + { + Arguments[name] = value.ToString(); + + return this; + } + + public PowerShellScript AddArgument(string name, long value) + { + Arguments[name] = value.ToString(); + + return this; + } + + public PowerShellScript RunAsAdmin() + { + AsAdmin = true; + + Process.StartInfo.Verb = "runas"; + Process.StartInfo.UseShellExecute = true; + + return this; + } + + public PowerShellScript IgnoreWow64Redirection() + { + IgnoreWow64 = true; + + return this; + } + + public int Execute() + { + var scriptBuilder = new StringBuilder(); + + var wow64Value = IntPtr.Zero; + + foreach (var variable in Variables) + { + scriptBuilder.AppendLine($"${variable.Name} = Convert-FromSerializedBase64 \"{Serialize(variable.Value)}\""); + } + + scriptBuilder.AppendLine(Contents); + + var path = ScriptHelper.SaveTempScript(scriptBuilder.ToString()); + + AddArgument("File", path); + + if (IgnoreWow64) + Wow64DisableWow64FsRedirection(ref wow64Value); + + Process.StartInfo.Arguments = String.Join(" ", Arguments.Select((name, value) => + { + return $"-{name} {value}"; + })); + + if (!String.IsNullOrEmpty(WorkingDirectory)) + Process.StartInfo.WorkingDirectory = WorkingDirectory; + + if (ShellExecute) + Process.StartInfo.UseShellExecute = true; + + if (AsAdmin) + { + Process.StartInfo.Verb = "runas"; + Process.StartInfo.UseShellExecute = true; + } + + Process.Start(); + Process.WaitForExit(); + + if (IgnoreWow64) + Wow64RevertWow64FsRedirection(ref wow64Value); + + if (File.Exists(path)) + File.Delete(path); + + return Process.ExitCode; + } + + public static string Serialize(T input) + { + // Use the PowerShell serializer to generate XML for our input. Then convert to base64 so we can put it on one line. + return Convert.ToBase64String(Encoding.UTF8.GetBytes(System.Management.Automation.PSSerializer.Serialize(input))); + } + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr); + } +} diff --git a/LANCommander.SDK/PowerShell/PowerShellVariable.cs b/LANCommander.SDK/PowerShell/PowerShellVariable.cs new file mode 100644 index 0000000..a7f78b8 --- /dev/null +++ b/LANCommander.SDK/PowerShell/PowerShellVariable.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK.PowerShell +{ + public class PowerShellVariable + { + public string Name { get; set; } + public object Value { get; set; } + public Type Type { get; set; } + + public PowerShellVariable(string name, object value, Type type) + { + Name = name; + Value = value; + Type = type; + } + } +}