using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Security.Principal; using System.ServiceModel; using System.ServiceModel.Description; using TGS.Interface; using TGS.Interface.Components; using TGS.Server.Security; namespace TGS.Server { /// /// The windows service the application runs as /// [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)] public sealed class Server : ITGSService, ITGConnectivity, ITGLanding, ITGInstanceManager, IDisposable { /// /// The logging ID used for events /// public const byte LoggingID = 0; /// /// The directory to use when importing a .NET settings based config /// public const string MigrationConfigDirectory = "C:\\TGSSettingUpgradeTempDir"; /// /// The service version based on the 's /// public static readonly string VersionString = String.Format("/tg/station 13 Server v{0}", Assembly.GetExecutingAssembly().GetName().Version); /// /// Singleton /// public static ILogger Logger { get; private set; } /// /// The for the /// public static ServerConfig Config { get; private set; } /// /// The directory to load and save s to /// static readonly string DefaultConfigDirectory = Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TGS.Server")).FullName; /// /// Begins user impersonation to allow proper restricted file access /// public static void BeginImpersonation() { WindowsIdentity.Impersonate(OperationContext.Current.ServiceSecurityContext.WindowsIdentity.Token); } /// /// Cancels WCF's user impersonation to allow clean access to writing log files /// public static void CancelImpersonation() { WindowsIdentity.Impersonate(IntPtr.Zero); } /// /// Checks an for illegal characters /// /// The name to check /// if contains no illegal characters, error message otherwise static string CheckInstanceName(string instanceName) { char[] bannedCharacters = { ';', '&', '=', '%' }; foreach (var I in bannedCharacters) if (instanceName.Contains(I.ToString())) return "Instance names may not contain the following characters: ';', '&', '=', or '%'"; return null; } /// /// The WCF host that contains connects to /// ServiceHost serviceHost; /// /// Map of to the respective hosting the /// IDictionary hosts; /// /// List of s in use /// IList UsedLoggingIDs = new List(); /// /// Construct a /// /// Command line arguments for the /// The to use public Server(string[] args, ILogger logger) { Logger = logger; Environment.CurrentDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Assembly.GetExecutingAssembly().GetName().Name)).FullName; //MOVE THIS POINTER BECAUSE ONE TIME I ALMOST ACCIDENTALLY NUKED MYSELF BY REFACTORING! http://imgur.com/zvGEpJD.png SetupConfig(); ChangePortFromCommandLine(args); SetupService(); SetupInstances(); OnlineAllHosts(); } /// /// Enumerates configured s. Detaches those that fail to load /// /// Each configured IEnumerable GetInstanceConfigs() { var pathsToRemove = new List(); lock (this) { var IPS = Config.InstancePaths; foreach (var I in IPS) { IInstanceConfig ic; try { ic = InstanceConfig.Load(I); } catch (Exception e) { Logger.WriteError(String.Format("Unable load instance config at path {0}. Error: {1} Detaching...", I, e.ToString()), EventID.InstanceInitializationFailure, LoggingID); pathsToRemove.Add(I); continue; } yield return ic; } foreach (var I in pathsToRemove) IPS.Remove(I); } } /// /// Overrides and saves the configured if requested by command line parameters /// /// The command line parameters for the void ChangePortFromCommandLine(string[] args) { for (var I = 0; I < args.Length - 1; ++I) if (args[I].ToLower() == "-port") { try { var res = Convert.ToUInt16(args[I + 1]); if (res == 0) throw new Exception("Cannot bind to port 0"); Config.RemoteAccessPort = res; } catch (Exception e) { throw new Exception("Invalid argument for \"-port\"", e); } Config.Save(DefaultConfigDirectory); break; } } /// /// Upgrades up the service configuration /// void SetupConfig() { Directory.CreateDirectory(DefaultConfigDirectory); try { Config = ServerConfig.Load(DefaultConfigDirectory); } catch { try { //assume we're upgrading Config = ServerConfig.Load(MigrationConfigDirectory); Config.Save(DefaultConfigDirectory); Helpers.DeleteDirectory(MigrationConfigDirectory); } catch { //new baby Config = new ServerConfig(); } } } /// /// Creates the for /// void SetupService() { serviceHost = CreateHost(this, Definitions.MasterInterfaceName); AddEndpoint(serviceHost, typeof(ITGConnectivity)); AddEndpoint(serviceHost, typeof(ITGInstanceManager)); AddEndpoint(serviceHost, typeof(ITGLanding)); AddEndpoint(serviceHost, typeof(ITGSService)); serviceHost.Authorization.ServiceAuthorizationManager = new RootAuthorizationManager(); //only admins can diddle us serviceHost.Authentication.ServiceAuthenticationManager = new AuthenticationHeaderDecoder(); } /// /// Opens all created s /// void OnlineAllHosts() { serviceHost.Open(); foreach (var I in hosts) I.Value.Open(); } /// /// Creates a for using the default pipe, CloseTimeout for the , and the configured /// /// The /// The URL to access components on the /// The created static ServiceHost CreateHost(object singleton, string endpointPostfix) { return new ServiceHost(singleton, new Uri[] { new Uri(String.Format("net.pipe://localhost/{0}", endpointPostfix)), new Uri(String.Format("https://localhost:{0}/{1}", Config.RemoteAccessPort, endpointPostfix)) }) { CloseTimeout = new TimeSpan(0, 0, 5) }; } /// /// Creates s for all s as listed in , detaches bad ones /// void SetupInstances() { hosts = new Dictionary(); var pathsToRemove = new List(); var seenNames = new List(); foreach (var I in GetInstanceConfigs()) { if (seenNames.Contains(I.Name)) { Logger.WriteError(String.Format("Instance at {0} has a duplicate name! Detaching...", I.Directory), EventID.InstanceInitializationFailure, LoggingID); pathsToRemove.Add(I.Directory); } if (I.Enabled && SetupInstance(I) == null) pathsToRemove.Add(I.Directory); else seenNames.Add(I.Name); } foreach (var I in pathsToRemove) Config.InstancePaths.Remove(I); } /// /// Unlocks a acquired with /// /// The to unlock void UnlockLoggingID(byte ID) { lock (UsedLoggingIDs) { UsedLoggingIDs.Remove(ID); } } /// /// Gets and locks a /// /// A logging ID for the must be released using byte LockLoggingID() { lock (UsedLoggingIDs) { for (byte I = 1; I < 100; ++I) if (!UsedLoggingIDs.Contains(I)) { UsedLoggingIDs.Add(I); return I; } } throw new Exception("All logging IDs in use!"); } /// /// Creates and starts a for a at /// /// The for the /// The inactive on success, on failure ServiceHost SetupInstance(IInstanceConfig config) { Instance instance; string instanceName; try { if (hosts.ContainsKey(config.Directory)) { var datInstance = ((Instance)hosts[config.Directory].SingletonInstance); Logger.WriteError(String.Format("Unable to start instance at path {0}. Has the same name as instance at path {1}. Detaching...", config.Directory, datInstance.ServerDirectory()), EventID.InstanceInitializationFailure, LoggingID); return null; } if (!config.Enabled) return null; var ID = LockLoggingID(); Logger.WriteInfo(String.Format("Instance {0} ({1}) assigned logging ID {2}", config.Name, config.Directory, ID), EventID.InstanceIDAssigned, ID); instanceName = config.Name; instance = new Instance(config, ID); } catch (Exception e) { Logger.WriteError(String.Format("Unable to start instance at path {0}. Detaching... Error: {1}", config.Directory, e.ToString()), EventID.InstanceInitializationFailure, LoggingID); return null; } var host = CreateHost(instance, String.Format("{0}/{1}", Definitions.InstanceInterfaceName, instanceName)); hosts.Add(instanceName, host); AddEndpoint(host, typeof(ITGConnectivity)); AddEndpoint(host, typeof(ITGAdministration)); AddEndpoint(host, typeof(ITGByond)); AddEndpoint(host, typeof(ITGChat)); AddEndpoint(host, typeof(ITGCompiler)); AddEndpoint(host, typeof(ITGConfig)); AddEndpoint(host, typeof(ITGDreamDaemon)); AddEndpoint(host, typeof(ITGInstance)); AddEndpoint(host, typeof(ITGInterop)); AddEndpoint(host, typeof(ITGRepository)); host.Authorization.ServiceAuthorizationManager = instance; host.Authentication.ServiceAuthenticationManager = new AuthenticationHeaderDecoder(); return host; } /// /// Adds a WCF endpoint for a component /// /// The service host to add the component to /// The type of the component void AddEndpoint(ServiceHost host, Type typetype) { var bindingName = typetype.Name; var endpint = host.AddServiceEndpoint(typetype, new NetNamedPipeBinding() { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = Definitions.TransferLimitLocal }, bindingName); foreach (OperationDescription od in endpint.Contract.Operations) od.OperationBehaviors.Add(new AddCallLoggerBehavior()); var httpsBinding = new BasicHttpsBinding() { SendTimeout = new TimeSpan(0, 0, 40), MaxReceivedMessageSize = Definitions.TransferLimitRemote }; var requireAuth = typetype.Name != typeof(ITGConnectivity).Name; host.AddServiceEndpoint(typetype, httpsBinding, bindingName); } /// /// Shuts down all active s and calls on it's /// void OnStop() { lock (this) { try { foreach (var I in hosts) { var host = I.Value; var instance = (Instance)host.SingletonInstance; host.Close(); instance.Dispose(); UnlockLoggingID(instance.LoggingID); } } catch (Exception e) { Logger.WriteError(e.ToString(), EventID.ServiceShutdownFail, LoggingID); } serviceHost.Close(); } Config.Save(DefaultConfigDirectory); } /// public void VerifyConnection() { } /// public void PrepareForUpdate() { foreach (var I in hosts) ((Instance)I.Value.SingletonInstance).Reattach(false); } /// public ushort RemoteAccessPort() { return Config.RemoteAccessPort; } /// public string SetRemoteAccessPort(ushort port) { if (port == 0) return "Cannot bind to port 0"; Config.RemoteAccessPort = port; return null; } /// public string Version() { return VersionString; } /// public bool SetPythonPath(string path) { if (!Directory.Exists(path)) return false; Config.PythonPath = Path.GetFullPath(path); return true; } /// public string PythonPath() { return Config.PythonPath; } /// public IList ListInstances() { var result = new List(); lock (this) foreach (var ic in GetInstanceConfigs()) result.Add(new InstanceMetadata { Name = ic.Name, Path = ic.Directory, Enabled = ic.Enabled, LoggingID = (byte)(ic.Enabled ? ((Instance)hosts[ic.Name].SingletonInstance).LoggingID : 0) }); return result; } /// public string CreateInstance(string Name, string path) { path = Helpers.NormalizePath(path); var res = CheckInstanceName(Name); if (res != null) return res; if (File.Exists(path) || Directory.Exists(path)) return "Cannot create instance at pre-existing path!"; lock (this) { if (Config.InstancePaths.Contains(path)) return String.Format("Instance at {0} already exists!", path); foreach (var oic in GetInstanceConfigs()) if (Name == oic.Name) return String.Format("Instance named {0} already exists!", oic.Name); IInstanceConfig ic; try { ic = new InstanceConfig(path) { Name = Name }; Directory.CreateDirectory(path); ic.Save(); Config.InstancePaths.Add(path); } catch (Exception e) { return e.ToString(); } return SetupOneInstance(ic); } } /// /// Starts and onlines an instance located at /// /// The for the /// on success, error message on failure string SetupOneInstance(IInstanceConfig config) { if (!config.Enabled) return null; try { var host = SetupInstance(config); if (host != null) host.Open(); else lock (this) Config.InstancePaths.Remove(config.Directory); return null; } catch (Exception e) { return "Instance set up but an error occurred while starting it: " + e.ToString(); } } /// public string ImportInstance(string path) { path = Helpers.NormalizePath(path); lock (this) { if (Config.InstancePaths.Contains(path)) return String.Format("Instance at {0} already exists!", path); if(!Directory.Exists(path)) return String.Format("There is no instance located at {0}!", path); IInstanceConfig ic; try { ic = InstanceConfig.Load(path); foreach(var oic in GetInstanceConfigs()) if(ic.Name == oic.Name) return String.Format("Instance named {0} already exists!", oic.Name); ic.Save(); Config.InstancePaths.Add(path); } catch (Exception e) { return e.ToString(); } return SetupOneInstance(ic); } } /// public bool InstanceEnabled(string Name) { lock(this) { return hosts.ContainsKey(Name); } } /// public string SetInstanceEnabled(string Name, bool enabled) { return SetInstanceEnabledImpl(Name, enabled, out string path); } /// /// Sets a 's enabled status /// /// The whom's status should be changed /// to enable the , to disable it /// The path to the modified /// on success, error message on failure string SetInstanceEnabledImpl(string Name, bool enabled, out string path) { path = null; lock (this) { var hostIsOnline = hosts.ContainsKey(Name); if (enabled) { if (hostIsOnline) return null; foreach (var ic in GetInstanceConfigs()) if (ic.Name == Name) { path = ic.Directory; ic.Enabled = true; ic.Save(); return SetupOneInstance(ic); } return String.Format("Instance {0} does not exist!", Name); } else { if (!hostIsOnline) return null; var host = hosts[Name]; hosts.Remove(Name); var inst = (Instance)host.SingletonInstance; host.Close(); path = inst.ServerDirectory(); inst.Offline(); inst.Dispose(); UnlockLoggingID(inst.LoggingID); return null; } } } /// public string RenameInstance(string name, string new_name) { if (name == new_name) return null; var res = CheckInstanceName(new_name); if (res != null) return res; lock (this) { //we have to check em all anyway IInstanceConfig the_droid_were_looking_for = null; foreach (var ic in GetInstanceConfigs()) if (ic.Name == name) { the_droid_were_looking_for = ic; break; } else if (ic.Name == new_name) return String.Format("There is already another instance named {0}!", new_name); if (the_droid_were_looking_for == null) return String.Format("There is no instance named {0}!", name); var ie = InstanceEnabled(name); if(ie) SetInstanceEnabled(name, false); the_droid_were_looking_for.Name = new_name; string result = ""; try { the_droid_were_looking_for.Save(); result = null; } catch(Exception e) { result = "Could not save instance config! Error: " + e.ToString(); } finally { if (ie) { var resRestore = SetInstanceEnabled(new_name, true); if (resRestore != null) result = (result + " " + resRestore).Trim(); } } return result; } } /// public string DetachInstance(string name) { lock (this) { var res = SetInstanceEnabledImpl(name, false, out string path); if (res != null) return res; if (path == null) //gotta find it ourselves foreach (var ic in GetInstanceConfigs()) if (ic.Name == name) { path = ic.Directory; break; } if (path == null) return String.Format("No instance named {0} exists!", name); Config.InstancePaths.Remove(path); return null; } } #region IDisposable Support /// /// To detect redundant calls /// private bool disposedValue = false; /// /// Implements the pattern. Calls /// /// if was called manually, if it was from the finalizer void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { OnStop(); } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. disposedValue = true; } } // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. // ~Server() { // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. // Dispose(false); // } /// /// Implements the pattern /// public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); // TODO: uncomment the following line if the finalizer is overridden above. // GC.SuppressFinalize(this); } #endregion } }