using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Text; using System.Threading.Tasks; using C4IT.FASD.Base; using C4IT.MultiLanguage; using C4IT.Security; using C4IT.Logging; using static C4IT.Logging.cLogManager; namespace FasdCockpitBase { public class cExternalToolExecutor { public interface iNamedParameter { string Title { get; set; } string GetValue(); (string Title, string Value) GetTitleValuePair(); } [DllImport("shell32.dll", EntryPoint = "ShellExecute")] public static extern long ShellExecute(int hwnd, string cmd, string file, string param1, string param2, int swmode); private static cCredential RemoteToolAlternateCredentials; public class cProcessResult { public cProcessResult(int returnCode) { ReturnCode = returnCode; } public int ReturnCode = -1; public string StandardOutput = null; public string StandardError = null; } static protected cCredential GetRemoteToolCredentials(string Title) { if (RemoteToolAlternateCredentials == null) { var Caption = cMultiLanguageSupport.GetItem("RemoteTool.Credentials.Caption"); Caption = string.Format(Caption, Title); var Message = cMultiLanguageSupport.GetItem("RemoteTool.Credentials.Message"); RemoteToolAlternateCredentials = cInputCredentials.GetCredentials(Caption, Message); } return RemoteToolAlternateCredentials; } static protected void AddFileSecurity(string fileName, string account, FileSystemRights rights, AccessControlType controlType) { try { FileSecurity fSecurity = File.GetAccessControl(fileName); fSecurity.AddAccessRule(new FileSystemAccessRule(account, rights, controlType)); File.SetAccessControl(fileName, fSecurity); } catch (Exception E) { LogException(E); } } static protected string ReplaceEnvironmentVariables(string input) { var output = input; try { var specialFolders = Enum.GetValues(typeof(Environment.SpecialFolder)).Cast(); foreach (var specialFolder in specialFolders) { var specialFolderName = "%" + specialFolder.ToString() + "%"; var specialFolderPath = Environment.GetFolderPath(specialFolder); output = output.Replace(specialFolderName, specialFolderPath); } output = Environment.ExpandEnvironmentVariables(output); } catch (Exception E) { LogException(E); } return output; } static protected async Task RunProcessAsync(string Cmd, string Params, cCredential Credentials, bool Hidden) { try { cProcessResult processResult = new cProcessResult(0); var process = createProcessInstance(Cmd, Params, Credentials, true, Hidden); TaskCompletionSource _resultAwaiter = null; _resultAwaiter = new TaskCompletionSource(); EventHandler resultEventHandler = null; resultEventHandler = (sender, args) => { process.Exited -= resultEventHandler; _resultAwaiter.SetResult(process.ExitCode); }; process.Exited += resultEventHandler; process.EnableRaisingEvents = true; try { process.Start(); } catch (Exception E) { LogException(E, LogLevels.Warning); return new cProcessResult(E.HResult); } var t1 = process.StandardOutput.ReadToEndAsync(); var t2 = process.StandardError.ReadToEndAsync(); var t3 = _resultAwaiter.Task; await Task.WhenAll(t1, t2, t3); processResult.StandardOutput = (await t1); processResult.StandardError = (await t2); processResult.ReturnCode = (await t3); return processResult; } catch (Exception E) { LogException(E); return new cProcessResult(E.HResult); } } static protected cProcessResult StartProcessAsync(string Cmd, string Params, cCredential Credentials, bool Hidden) { try { using (var process = createProcessInstance(Cmd, Params, Credentials, false, Hidden)) { var _s = process.Start(); } return new cProcessResult(0); } catch (Exception E) { LogException(E, LogLevels.Warning); return new cProcessResult(E.HResult); } } static protected Process createProcessInstance(string Cmd, string Params, cCredential Credentials, bool Wait, bool Hidden) { try { var processInformation = new ProcessStartInfo(Cmd, Params); processInformation.UseShellExecute = false; if (Credentials != null) { processInformation.LoadUserProfile = true; processInformation.UserName = Credentials.User; processInformation.Password = Credentials.Password; processInformation.Domain = Credentials.Domain; } if (Hidden) { processInformation.CreateNoWindow = true; processInformation.WindowStyle = ProcessWindowStyle.Hidden; } if (Wait) { processInformation.RedirectStandardOutput = true; processInformation.RedirectStandardError = true; } var process = new Process() { StartInfo = processInformation }; return process; } catch (Exception E) { LogException(E); } return null; } static public string ReplaceParameters(string input, Dictionary NamedParameters, bool ReplaceEnvironment, Dictionary ParameterDictionary = null) { string output = input; try { if (input != null) { if (ParameterDictionary != null) { foreach (var parameter in ParameterDictionary) { var _str = parameter.Key.GetValueString(parameter.Value); output = output.Replace("%" + parameter.Key.ParameterName + "%", _str); } } if (NamedParameters != null) foreach (var namedParameter in NamedParameters) { try { var parameterName = namedParameter.Key; var namedParameterKeyValuePair = namedParameter.Value.GetTitleValuePair(); var parameterLabel = namedParameterKeyValuePair.Title; var parameterValue = namedParameterKeyValuePair.Value; output = output.Replace("%" + parameterName + ".Label%", parameterLabel); output = output.Replace("%" + parameterName + ".Value%", parameterValue); } catch { } } if (ReplaceEnvironment) output = ReplaceEnvironmentVariables(output); } } catch (Exception E) { LogException(E); } return output; } public static async Task StartLocalActionAsync(cFasdQuickActionLocal localAction, Dictionary NamedParameters, string ProgramTitle, Dictionary ParameterDictionary = null) { cProcessResult _resultProcess = new cProcessResult(0); cCredential Credentials = null; if (localAction.StartWithAlternateCredentials) Credentials = GetRemoteToolCredentials(ProgramTitle); const string constPowershellPath = @"%System%\WindowsPowerShell\v1.0\powershell.exe"; string parameters = null; string cmd = null; string scriptName = null; bool dontUseShell = true; bool isHidden = false; bool isScript = false; bool doWait = (localAction.ColumnOutputFormattings?.Count > 0); if (localAction is cFasdQuickActionLocalScript quickActionScript) { isScript = true; isHidden = true; doWait = true; cmd = constPowershellPath; scriptName = Path.GetTempFileName(); if (File.Exists(scriptName)) File.Delete(scriptName); scriptName += ".ps1"; if (quickActionScript.IsBase64) File.WriteAllBytes(scriptName, quickActionScript.ScriptBinary); else File.WriteAllText(scriptName, quickActionScript.Script, Encoding.Unicode); if (localAction.StartWithAlternateCredentials && (Credentials != null)) AddFileSecurity(scriptName, Credentials.GetAccount(true), FileSystemRights.Read, AccessControlType.Allow); parameters = $"-NoLogo -ExecutionPolicy bypass -file \"{scriptName}\""; if (localAction.Parameters != null) parameters += " " + localAction.Parameters; } else if (localAction is cFasdQuickActionLocalCmd quickActionCmd) { cmd = quickActionCmd.Cmd; parameters = quickActionCmd.Parameters; dontUseShell = quickActionCmd.DontUseShell; } else return _resultProcess; cmd = ReplaceEnvironmentVariables(cmd); if (parameters != null) parameters = ReplaceParameters(parameters, NamedParameters, true, ParameterDictionary); try { if (!dontUseShell && !localAction.StartWithAlternateCredentials) { var PRet = (int)ShellExecute(0, "open", cmd, parameters, null, 5); /* * The Microsoft docs states as following regarding the return value of a ShellExecute: * https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutea * * If the function succeeds, it returns a value greater than 32. * If the function fails, it returns an error value that indicates the cause of the failure. * The return value is cast as an HINSTANCE for backward compatibility with 16-bit Windows applications. It is not a true HINSTANCE, however. * It can be cast only to an INT_PTR and compared to either 32 or the following error codes below. */ const int highestFailureReturnCode = 32; // Our internal cPorcessResult is built that way a ReturnCode of 0 is considered as successfull. // This has to be mapped to the HINSTANCE result code above. if (PRet > highestFailureReturnCode) PRet = 0; _resultProcess = new cProcessResult(PRet); } else { await Task.Run(async () => { if (localAction.RequireAdministrator && localAction.StartWithAlternateCredentials) { cmd = ReplaceEnvironmentVariables("%System%\\WindowsPowerShell\\v1.0\\powershell.exe"); var argumentList = parameters.Replace("\"", "\"\""); var Cmd = $"Start-Process \"{cmd}\" -WindowStyle Hidden -Wait -Verb \"runas\" -ArgumentList \"{argumentList}\""; var parameterByteArray = Encoding.Unicode.GetBytes(Cmd); parameters = Convert.ToBase64String(parameterByteArray); parameters = "-NonInteractive -NoLogo -ExecutionPolicy bypass -EncodedCommand " + parameters; isHidden = true; } if (doWait) _resultProcess = await RunProcessAsync(cmd, parameters, Credentials, isHidden); else _resultProcess = StartProcessAsync(cmd, parameters, Credentials, isHidden); }); } } catch (Exception E) { LogException(E); } if (isScript) { try { File.Delete(scriptName); } catch { } ; } return _resultProcess; } } }