/* * MIT License * * Copyright (c) 2017 Michael VanOverbeek and ShiftOS devs * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; using static ShiftOS.Engine.SaveSystem; namespace ShiftOS.Engine { /// /// Backend for the ShiftOS terminal. /// public static class TerminalBackend { /// /// Occurs when a command is processed. /// public static event Action CommandProcessed; /// /// Gets or sets whether the current command is elevated. /// public static bool Elevated { get; set; } /// /// Parses command-line arguments using the ShiftOS syntax and stores them in a , removing the parsed text from the original string. /// /// The text to parse. /// containing the parsed arguments. public static Dictionary GetArgs(ref string text) { bool shouldParse = false; int argStart = 0; if (text.Contains("{")) { shouldParse = true; argStart = text.IndexOf('{'); } if (shouldParse == false) { string replacement = Regex.Replace(text, @"\t|\n|\r", ""); text = replacement + "{}"; shouldParse = true; argStart = text.IndexOf('{'); } string args = text.Substring(argStart, text.Length - argStart); text = text.Remove(argStart, text.Length - argStart).Replace(" ", ""); return JsonConvert.DeserializeObject>(args); } /// /// String representing the last command entered by the user. /// public static string LastCommand = ""; /// /// Gets the output of the last command. /// public static string LastCommandBuffer { get; private set; } /// /// Invokes a ShiftOS terminal command. /// /// The command name. /// The command arguments. /// Whether the command should be sent through Remote Terminal Session (RTS). public static void InvokeCommand(string command, Dictionary arguments, bool isRemote = false) { try { bool commandWasClient = RunClient(command, arguments, isRemote); if (!commandWasClient) { Console.WriteLine("{ERR_COMMANDNOTFOUND}"); } CommandProcessed?.Invoke(command, JsonConvert.SerializeObject(arguments)); } catch (Exception ex) { Console.WriteLine("{ERR_SYNTAXERROR}"); PrefixEnabled = true; } } /// /// Transforms a of arguments to a . /// /// The original argument dictionary to convert. /// The converted dictionary. public static string GetSentArgs(Dictionary argss) { Dictionary args = new Dictionary(); foreach (KeyValuePair arg in argss) { args[arg.Key] = arg.Value; } return JsonConvert.SerializeObject(args); } public class TerminalCommand { public override int GetHashCode() { int hash = 0; foreach (char c in ToString()) { hash += (int)c; } return hash; } public Command CommandInfo { get; set; } public List RequiredArguments { get; set; } public string Dependencies { get; set; } public MethodInfo CommandHandler; public Type CommandType; public override string ToString() { StringBuilder sb = new StringBuilder(); sb.Append(this.CommandInfo.name); if (this.RequiredArguments.Count > 0) { sb.Append(" "); foreach (var arg in RequiredArguments) { sb.Append("--" + arg); sb.Append(" "); if (RequiredArguments.IndexOf(arg) < RequiredArguments.Count - 1) sb.Append(','); } sb.Append("}"); } sb.Append("|"); sb.Append(CommandHandler.Name + "()"); return sb.ToString(); } public bool RequiresElevation { get; set; } public virtual void Invoke(Dictionary args) { List errors = new List(); bool requiresAuth = false; if (!KernelWatchdog.IsSafe(this)) { if (SaveSystem.CurrentUser.Permissions == Objects.UserPermissions.Admin) requiresAuth = true; else errors.Add("You can't run this command - you do not have permission."); } if (errors.Count > 0) { foreach (var error in errors) { Console.WriteLine("Command error: " + error); } return; } if (requiresAuth) { Infobox.PromptText("Enter your password.", "This command requires you to have elevated permissions. Please enter your password to confirm this action.", (pass) => { if (pass == SaveSystem.CurrentUser.Password) { var uname = SaveSystem.CurrentUser.Username; SaveSystem.CurrentUser = SaveSystem.CurrentSave.Users.FirstOrDefault(x => x.Username == "root"); try { var h = CommandHandler; h.Invoke(null, new[] { args }); } catch { var h = CommandHandler; h.Invoke(null, null); } SaveSystem.CurrentUser = SaveSystem.CurrentSave.Users.FirstOrDefault(x => x.Username == uname); } else { Infobox.Show("Access denied.", "Incorrect password provided. The command will not run."); } }, true); } try { CommandHandler.Invoke(null, new[] { args }); } catch { CommandHandler.Invoke(null, null); } } } public class WinOpenCommand : TerminalCommand { public Type ShiftOSWindow { get; set; } public override void Invoke(Dictionary args) { AppearanceManager.SetupWindow((IShiftOSWindow)Activator.CreateInstance(ShiftOSWindow, null)); } } public class MemoryTextWriter : System.IO.TextWriter { public override Encoding Encoding { get { return Encoding.Unicode; } } private StringBuilder sb = null; public MemoryTextWriter() { sb = new StringBuilder(); } public override string ToString() { return sb.ToString(); } public override void Write(char value) { sb.Append(value); } public override void WriteLine() { sb.AppendLine(); } public override void Write(string value) { sb.Append(value); } public override void Close() { sb.Clear(); sb = null; base.Close(); } public override void WriteLine(string value) { sb.AppendLine(value); } } public static List Commands { get; private set; } public static void PopulateTerminalCommands() { Commands = new List(); foreach (var type in ReflectMan.Types) { if (type.GetInterfaces().Contains(typeof(IShiftOSWindow))) { var winopenattrib = type.GetCustomAttributes(false).FirstOrDefault(x => x is WinOpenAttribute) as WinOpenAttribute; if(winopenattrib != null) { var winc = new WinOpenCommand(); winc.CommandType = type; var rupg = type.GetCustomAttributes().FirstOrDefault(x => x is RequiresUpgradeAttribute) as RequiresUpgradeAttribute; if (rupg != null) winc.Dependencies = rupg.Upgrade; winc.CommandInfo = new Engine.Command(winopenattrib.ID, "", "Opens the \"" + winopenattrib.ID + " program."); winc.RequiredArguments = new List(); winc.RequiresElevation = false; winc.ShiftOSWindow = type; var ambiguity = Commands.FirstOrDefault(x => x.CommandInfo.name == winc.CommandInfo.name); if (ambiguity != null) throw new Exception("Ambiguity error. The program " + winc.CommandInfo.name + " collides with another program or terminal command with the same name. Please either change the already-existing program/command, or change this one's WinOpenAttribute value to compensate."); Commands.Add(winc); } } foreach (var mth in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) { var cmd = mth.GetCustomAttributes(false).FirstOrDefault(x => x is Command); if (cmd != null) { var tc = new TerminalCommand(); tc.RequiresElevation = !(type.GetCustomAttributes(false).FirstOrDefault(x => x is KernelModeAttribute) == null); tc.CommandInfo = cmd as Command; tc.RequiresElevation = tc.RequiresElevation || !(mth.GetCustomAttributes(false).FirstOrDefault(x => x is KernelModeAttribute) == null); tc.RequiredArguments = new List(); foreach (var arg in mth.GetCustomAttributes(false).Where(x => x is RequiresArgument)) { var rarg = arg as RequiresArgument; tc.RequiredArguments.Add(rarg.argument); } var rupg = mth.GetCustomAttributes(false).FirstOrDefault(x => x is RequiresUpgradeAttribute) as RequiresUpgradeAttribute; if (rupg != null) tc.Dependencies = rupg.Upgrade; else tc.Dependencies = ""; tc.CommandType = type; tc.CommandHandler = mth; var ambiguity = Commands.FirstOrDefault(x => x.CommandInfo.name == tc.CommandInfo.name); if (ambiguity != null) throw new Exception("Command ambiguity error. You can't have two commands with the same name: " + $"{tc} == {ambiguity}"); if (!Commands.Contains(tc)) Commands.Add(tc); } } } Console.WriteLine("[termdb] " + Commands.Count + " commands found."); } /// /// Invokes a ShiftOS terminal command. /// /// The full command text in regular ShiftOS syntax /// Whether the command should be sent through Remote Terminal Session (RTS). public static void InvokeCommand(string text, bool isRemote = false) { if (string.IsNullOrWhiteSpace(text)) return; var tw = new MemoryTextWriter(); Console.SetOut(tw); try { var args = GetArgs(ref text); bool commandWasClient = RunClient(text, args, isRemote); if (!commandWasClient) { Console.WriteLine("Error: Command not found."); } CommandProcessed?.Invoke(text, GetSentArgs(args)); } catch (Exception ex) { Console.WriteLine($"Command parse error: {ex.Message}"); PrefixEnabled = true; } string buffer = tw.ToString(); LastCommandBuffer = buffer; Console.SetOut(new TerminalTextWriter()); if(!isRemote) Console.Write(buffer); } /// /// Gets or sets whether the user prefix is printed after a command completes. /// public static bool PrefixEnabled { get; set; } /// /// Gets or sets whether the user is in a story plot, and thus, the terminal input should be disabled. /// public static bool InStory { get; set; } /// /// Another latest command string. /// public static string latestCommmand = ""; /// /// Occurs when the engine requests a Terminal to be open. /// public static event EmptyEventHandler TerminalRequested; /// /// Opens a Terminal. /// internal static void OpenTerminal() { TerminalRequested?.Invoke(); } /// /// Determines if the specified command method can be ran in RTS /// /// The method to scan /// Is the user in an RTS session? /// Whether the command can be run. public static bool CanRunRemotely(MethodInfo mth, bool isRemote) { if (!isRemote) return true; foreach (var attr in mth.GetCustomAttributes(false)) { if (attr is RemoteLockAttribute) return false; } return true; } /// /// Runs a command on the client. /// /// The command text. /// The command arguments. /// Whether the command should be ran through RTS. /// Whether the command ran successfully. public static bool RunClient(string text, Dictionary argss, bool isRemote = false) { Dictionary args = new Dictionary(); foreach (KeyValuePair arg in argss) { args[arg.Key] = arg.Value; } return RunClient(text, args, isRemote); } /// /// Runs a command on the client. /// /// The command text. /// The command arguments. /// Whether the command should be run in RTS. /// Whether the command ran successfully. public static bool RunClient(string text, Dictionary args, bool isRemote = false) { latestCommmand = text; //Console.WriteLine(text + " " + "{" + string.Join(",", args.Select(kv => kv.Key + "=" + kv.Value).ToArray()) + "}" + " " + isRemote); var cmd = Commands.FirstOrDefault(x => Localization.Parse(x.CommandInfo.name) == text); if (cmd == null) return false; if (!Shiftorium.UpgradeInstalled(cmd.Dependencies)) return false; bool res = false; foreach (var arg in cmd.RequiredArguments) { if (!args.ContainsKey(arg)) { res = true; Console.WriteLine("You are missing an argument with the key \"" + arg + "\"."); } } if (res == true) return true; try { cmd.Invoke(args); } catch(Exception ex) { Console.WriteLine("Command error: " + ex.Message); } return true; } /// /// Prints the user prompt to the terminal. /// public static void PrintPrompt() { Console.WriteLine(); if (SaveSystem.CurrentSave != null && CurrentUser != null) { ConsoleEx.BackgroundColor = SkinEngine.LoadedSkin.TerminalBackColorCC; ConsoleEx.Italic = false; ConsoleEx.Underline = false; ConsoleEx.ForegroundColor = ConsoleColor.Magenta; ConsoleEx.Bold = true; Console.Write(SaveSystem.CurrentUser.Username); ConsoleEx.Bold = false; ConsoleEx.ForegroundColor = ConsoleColor.Gray; Console.Write("@"); ConsoleEx.Italic = true; ConsoleEx.Bold = true; ConsoleEx.ForegroundColor = ConsoleColor.Yellow; Console.Write(SaveSystem.CurrentSave.SystemName); ConsoleEx.Italic = false; ConsoleEx.Bold = false; ConsoleEx.ForegroundColor = ConsoleColor.Gray; Console.Write(":~"); Console.ForegroundColor = ConsoleColor.White; ConsoleEx.Italic = true; if (KernelWatchdog.InKernelMode == true) Console.Write("#"); else Console.Write("$"); ConsoleEx.Italic = false; ConsoleEx.Bold = false; ConsoleEx.ForegroundColor = SkinEngine.LoadedSkin.TerminalForeColorCC; Console.Write(" "); ConsoleEx.Flush(); } } /// /// Static constructor for . /// static TerminalBackend() { ServerMessageReceived onMessageReceived = (msg) => { if (msg.Name == "trm_invokecommand") { string text3 = ""; string text4 = msg.Contents; if (TerminalBackend.PrefixEnabled) { text3 = text4.Remove(0, $"{SaveSystem.CurrentUser.Username}@{SaveSystem.CurrentSave.SystemName}:~$ ".Length); } IsForwardingConsoleWrites = true; if (TerminalBackend.InStory == false) { TerminalBackend.InvokeCommand(text3, true); } if (TerminalBackend.PrefixEnabled) { Console.Write($"{SaveSystem.CurrentUser.Username}@{SaveSystem.CurrentSave.SystemName}:~$ "); } IsForwardingConsoleWrites = false; } else if (msg.Name == "pleasewrite") { Console.Write(msg.Contents); } else if (msg.Name == "handshake_from") { var a = JsonConvert.DeserializeObject>(msg.Contents); string uName = a["username"] as string; string pass = a["password"] as string; string sys = a["sysname"] as string; string guid = msg.GUID; if (SaveSystem.CurrentUser.Username == uName && SaveSystem.CurrentSave.Password == pass && CurrentSave.SystemName == sys) { ForwardGUID = guid; ServerManager.SendMessage("trm_handshake_accept", $@"{{ guid: ""{ServerManager.thisGuid}"", target: ""{guid}"" }}"); IsForwardingConsoleWrites = true; InvokeCommand("sos.status"); Console.Write($"{SaveSystem.CurrentUser.Username}@{SaveSystem.CurrentSave.SystemName}:~$ "); IsForwardingConsoleWrites = false; } } }; ServerManager.MessageReceived += onMessageReceived; } /// /// Gets whether the terminal backend is forwarding console write requests through RTS to a remote client. /// public static bool IsForwardingConsoleWrites { get; internal set; } /// /// Gets the RTS forward GUID. /// public static string ForwardGUID { get; internal set; } /// /// Occurs when the user inputs text in a Terminal. /// public static event TextSentEventHandler TextSent; /// /// Fakes the user inputting text to a Terminal. /// /// The text to input. public static void SendText(string text) { TextSent?.Invoke(text); } } }