using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Web.Script.Serialization; using System.Web.Security; using TGServerService.ChatCommands; using TGServiceInterface; using TGServiceInterface.Components; namespace TGServerService { //handles talking between the world and us sealed partial class ServerInstance : ITGInterop { object topicLock = new object(); const int CommsKeyLen = 64; string serviceCommsKey; //regenerated every DD restart //range of supported api versions const int AllowedMajorAPIVersion = 2; Version GameAPIVersion; const string SPInstanceName = "server_instance"; //See code/modules/server_tools/server_tools.dm for command switch const string SCHardReboot = "hard_reboot"; //requests that dreamdaemon restarts when the round ends const string SCGracefulShutdown = "graceful_shutdown"; //requests that dreamdaemon stops when the round ends const string SCWorldAnnounce = "world_announce"; //sends param 'message' to the world const string SCListCustomCommands = "list_custom_commands"; //Get a list of commands supported by the server const string SCAPICompat = "api_compat"; //Tells the server we understand each other const string SCPlayerCount = "client_count"; //Gets the number of connected client /// /// String returned when a command completes successfully with no output /// const string SRetSuccess = "SUCCESS"; const string SRKillProcess = "killme"; const string SRIRCBroadcast = "irc"; const string SRIRCAdminChannelMessage = "send2irc"; const string SRWorldReboot = "worldreboot"; const string SRAPIVersion = "api_ver"; const string CCPHelpText = "help_text"; const string CCPAdminOnly = "admin_only"; const string CCPRequiredParameters = "required_parameters"; /// /// The file name of the .dll that contains the bridge class /// const string BridgeDLLName = "TGDreamDaemonBridge.dll"; /// /// The namespace that contains the bridge class. Used for reflection /// const string DreamDaemonBridgeNamespace = "TGDreamDaemonBridge"; /// /// The bridge class. Used for reflection /// const string DreamDaemonBridgeType = DreamDaemonBridgeNamespace + ".DreamDaemonBridge"; List ServerChatCommands; void LoadServerChatCommands() { if (DaemonStatus() != DreamDaemonStatus.Online) return; var json = SendCommand(SCListCustomCommands); if (String.IsNullOrWhiteSpace(json)) return; List tmp = new List(); try { foreach(var I in new JavaScriptSerializer().Deserialize>>(json)) tmp.Add(new ServerChatCommand(I.Key, (string)I.Value[CCPHelpText], ((int)I.Value[CCPAdminOnly]) == 1, (int)I.Value[CCPRequiredParameters])); ServerChatCommands = tmp; } catch { } } //raw command string sent here via world.ExportService void HandleCommand(string cmd) { var splits = new List(cmd.Split(' ')); cmd = splits[0]; splits.RemoveAt(0); bool APIValid; lock (topicLock) { APIValid = CheckAPIVersionConstraints(); } if (!APIValid && cmd != SRAPIVersion) return; //SPEAK THE LANGUAGE!!! switch (cmd) { case SRIRCBroadcast: SendMessage("GAME: " + String.Join(" ", splits), MessageType.GameInfo); break; case SRKillProcess: KillMe(); break; case SRIRCAdminChannelMessage: SendMessage("RELAY: " + String.Join(" ", splits), MessageType.AdminInfo); break; case SRWorldReboot: WriteInfo("World Rebooted", EventID.WorldReboot); WriteCurrentDDLog("World rebooted"); ServerChatCommands = null; ChatConnectivityCheck(); lock (CompilerLock) { if (UpdateStaged) { UpdateStaged = false; lock (topicLock) { GameAPIVersion = null; //needs updating } WriteInfo("Staged update applied", EventID.ServerUpdateApplied); } } break; case SRAPIVersion: lock (topicLock) { try { GameAPIVersion = new Version(splits[0]); if (!CheckAPIVersionConstraints()) throw new Exception(); } catch { WriteWarning(String.Format("API version of the game ({0}) is incompatible with the current supported API versions (3.{2}.x.x). Interop disabled.", splits.Count > 1 ? splits[1] : "NULL", AllowedMajorAPIVersion), EventID.APIVersionMismatch); GameAPIVersion = null; break; } } //This needs to be done asyncronously otherwise DD won't be able to process it, because it's waiting for THIS THREAD to return ThreadPool.QueueUserWorkItem(_ => SendCommand(SCAPICompat)); break; } } public string SendCommand(string cmd) { lock (watchdogLock) { if (currentStatus != DreamDaemonStatus.Online) return "Error: Server Offline!"; return SendTopic(String.Format("serviceCommsKey={0};command={1}", serviceCommsKey, cmd), currentPort); } } public int PlayerCount() { try { return Convert.ToInt32(SendCommand(SCPlayerCount)); } catch { return -1; } } //requires topiclock bool CheckAPIVersionConstraints() { //major will never change for all of TGS3 //we treat minor as major, build as minor, and revision as patch return !(GameAPIVersion == null || GameAPIVersion.Minor != AllowedMajorAPIVersion); } //Fuckery to diddle byond with the right packet to accept our girth string SendTopic(string topicdata, ushort port) { //santize the escape characters in accordance with http://www.byond.com/docs/ref/info.html#/proc/params2list lock (topicLock) { if (!CheckAPIVersionConstraints()) return "Incompatible API!"; StringBuilder stringPacket = new StringBuilder(); stringPacket.Append((char)'\x00', 8); stringPacket.Append('?' + topicdata); stringPacket.Append((char)'\x00'); string fullString = stringPacket.ToString(); var packet = Encoding.ASCII.GetBytes(fullString); packet[1] = 0x83; var FinalLength = packet.Length - 4; if (FinalLength > UInt16.MaxValue) return "Error: Topic too long"; var lengthBytes = BitConverter.GetBytes((ushort)FinalLength); packet[2] = lengthBytes[1]; //fucking endianess packet[3] = lengthBytes[0]; var returnedString = "NULL"; var returnedData = new byte[UInt16.MaxValue]; using (var topicSender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { SendTimeout = 5000, ReceiveTimeout = 5000 }) try { topicSender.Connect(IPAddress.Loopback, port); topicSender.Send(packet); try { topicSender.Receive(returnedData); var raw_string = Encoding.ASCII.GetString(returnedData).TrimEnd(new char[] { (char)0 }).Trim(); if (raw_string.Length > 6) returnedString = raw_string.Substring(5, raw_string.Length - 5).Trim(); } catch { returnedString = "Topic recieve error!"; } finally { topicSender.Shutdown(SocketShutdown.Both); } } catch { return "Topic delivery failed!"; } return returnedString; } } //Every time we make a new DD process we generate a new comms key for security //It's in world.params['server_service'] void GenCommsKey() { var charsToRemove = new string[] { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "-", "+", "=", "[", "{", "]", "}", ";", ":", "<", ">", "|", ".", "/", "?" }; serviceCommsKey = String.Empty; do { var tmp = Membership.GeneratePassword(CommsKeyLen, 0); foreach (var c in charsToRemove) tmp = tmp.Replace(c, String.Empty); serviceCommsKey += tmp; } while (serviceCommsKey.Length < CommsKeyLen); serviceCommsKey = serviceCommsKey.Substring(0, CommsKeyLen); WriteInfo("Service Comms Key set to: " + serviceCommsKey, EventID.CommsKeySet); } /// public bool InteropMessage(string command) { try { HandleCommand(command); return true; } catch(Exception e) { WriteWarning(String.Format("Handle command for \"{0}\" failed: {1}", command, e.ToString()), EventID.InteropCallException); return false; } } } }