using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Security; using System.Reflection; using System.Security.Principal; using System.ServiceModel; using TGServiceInterface.Components; namespace TGServiceInterface { /// /// Main for communicating the /// public interface IInterface : IDisposable { /// /// The name of the current instance in use. Defaults to /// string InstanceName { get; } /// /// If this is set, we will try and connect to an HTTPS server running at this address /// string HTTPSURL { get; } /// /// The port used to connect to the /// ushort HTTPSPort { get; } /// /// Checks if the is setup for a remote connection /// bool IsRemoteConnection { get; } /// /// Targets as the instance to use with . Closes all connections to any previous instance /// /// The name of the instance to connect to /// If set to , skips the connectivity and authentication checks, sets , and returns /// The apporopriate ConnectivityLevel ConnectToInstance(string instanceName = null, bool skipChecks = false); /// /// Returns if the interface being used to connect to a service does not have the same release version as the service /// /// An error message to display to the user should this function return /// if the interface being used to connect to a service does not have the same release version as the service bool VersionMismatch(out string errorMessage); /// /// Returns the requested component for the instance . This does not guarantee a successful connection. /// /// The component to retrieve /// The correct component T GetComponent(); /// /// Returns a root service component /// /// The component for the service T GetServiceComponent(); /// /// Used to test if the is avaiable on the target machine. Note that state can change at any time and any call into the may throw an exception because of communcation errors /// /// on successful connection, error message on failure ConnectivityLevel ConnectionStatus(); /// /// Used to test if the is avaiable on the target machine. Note that state can change at any time and any call into the may throw an exception because of communcation errors /// /// String of the error that prevented an elevated connectivity level /// The apporopriate ConnectivityLevel ConnectionStatus(out string error); } /// sealed public class Interface : IInterface { /// /// List of s that can be used with /// public static readonly IList ValidServiceInterfaces = new List { typeof(ITGSService), typeof(ITGInstanceManager), typeof(ITGConnectivity), typeof(ITGLanding) }; /// /// List of s that can be used with /// public static readonly IList ValidInstanceInterfaces = CollectComponents(); /// /// The maximum message size to and from a local server /// public const long TransferLimitLocal = Int32.MaxValue; //2GB can't go higher /// /// The maximum message size to and from a remote server /// public const long TransferLimitRemote = 10485760; //10 MB /// /// Base name of communication URLs /// public const string MasterInterfaceName = "TGStationServerService"; /// /// Base name of instance URLs /// public const string InstanceInterfaceName = MasterInterfaceName + "/Instance"; /// public string InstanceName { get; private set; } /// /// If this is set, we will try and connect to an HTTPS server running at this address /// readonly string _HTTPSURL; /// public string HTTPSURL { get { return _HTTPSURL; } } /// /// The port used to connect to the /// readonly ushort _HTTPSPort; /// public ushort HTTPSPort { get { return _HTTPSPort; } } /// /// Username for remote operations /// readonly string HTTPSUsername; /// /// Password for remote operations /// readonly string HTTPSPassword; /// /// Associated list of open s keyed by type name. A in this list may close or fault at any time. Must be locked before being accessed /// IDictionary ChannelFactoryCache = new Dictionary(); /// /// Returns a of s that can be used with the service /// /// A of s that can be used with the service static IList CollectComponents() { var ConnectivityComponent = typeof(ITGConnectivity); //find all interfaces in this assembly in this namespace that have the service contract attribute var query = from t in Assembly.GetExecutingAssembly().GetTypes() where t.IsInterface && t.Namespace == ConnectivityComponent.Namespace && t.GetCustomAttribute(typeof(ServiceContractAttribute)) != null && (t == ConnectivityComponent || !ValidServiceInterfaces.Contains(t)) select t; return query.ToList(); } /// /// Sets the function called when a remote login fails due to the server having an invalid SSL cert /// /// The to be called when a remote login is attempted while the server posesses a bad certificate. Passed a of error information about the and should return if it the connection should be made anyway public static void SetBadCertificateHandler(Func handler) { ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, error) => { string ErrorMessage; switch (error) { case SslPolicyErrors.None: return true; case SslPolicyErrors.RemoteCertificateChainErrors: ErrorMessage = "There are certificate chain errors."; break; case SslPolicyErrors.RemoteCertificateNameMismatch: ErrorMessage = "The certificate name does not match."; break; case SslPolicyErrors.RemoteCertificateNotAvailable: ErrorMessage = "The certificate doesn't exist in the trust store."; break; default: ErrorMessage = "An unknown error occurred."; break; } ErrorMessage = String.Format("The server's certificate failed to verify! Error: {0} Cert: {1}", ErrorMessage, cert.ToString()); return handler(ErrorMessage); }; } /// /// Construct an for a local connection /// public Interface() { } /// /// Construct an for a remote connection /// /// The address of the remote server /// The port the remote server runs on /// Windows account username for the remote server /// Windows account password for the remote server public Interface(string address, ushort port, string username, string password) { _HTTPSURL = address; _HTTPSPort = port; HTTPSUsername = username; HTTPSPassword = password; } /// /// Constructs an that connects to the same as some /// /// Another to copy settings from public Interface(Interface other) : this(other.HTTPSURL, other.HTTPSPort, other.HTTPSUsername, other.HTTPSPassword) { } /// public ConnectivityLevel ConnectToInstance(string instanceName = null, bool skipChecks = false) { if (instanceName == null) instanceName = InstanceName; if (!skipChecks && !ConnectionStatus().HasFlag(ConnectivityLevel.Connected)) return ConnectivityLevel.None; var prevInstance = InstanceName; if (prevInstance != instanceName) CloseAllChannels(false); InstanceName = instanceName; if (skipChecks) return ConnectivityLevel.Connected; try { GetComponent().VerifyConnection(); } catch { InstanceName = prevInstance; return ConnectivityLevel.None; } try { GetComponent().ServerDirectory(); } catch { return ConnectivityLevel.Connected; } try { GetComponent().GetCurrentAuthorizedGroup(); return ConnectivityLevel.Administrator; } catch { return ConnectivityLevel.Authenticated; } } /// public bool IsRemoteConnection { get { return HTTPSURL != null; } } /// /// Closes all s stored in and clears it /// /// If set to , doesn't clear the channels that are used by void CloseAllChannels(bool includingRoot) { string[] RootThings = { typeof(ITGSService).Name, 'S' + typeof(ITGConnectivity).Name }; lock (ChannelFactoryCache) { var toRemove = new List(); foreach (var I in ChannelFactoryCache) { if (RootThings.Contains(I.Key)) continue; var cf = I.Value; try { cf.Closed += ChannelFactory_Closed; cf.Close(); } catch { cf.Abort(); } toRemove.Add(I.Key); } foreach (var I in toRemove) ChannelFactoryCache.Remove(I); } } /// public bool VersionMismatch(out string errorMessage) { var splits = GetServiceComponent().Version().Split(' '); var theirs = new Version(splits[splits.Length - 1].Substring(1)); var ours = new Version(FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).FileVersion); if(theirs.Major != ours.Major || theirs.Minor != ours.Minor || theirs.Revision != ours.Revision) //don't care about the patch level { errorMessage = String.Format("Version mismatch between interface version ({0}) and service version ({1}). Some functionality may crash this program.", ours, theirs); return true; } errorMessage = null; return false; } /// /// Disposes a closed /// /// The channel factory that was closed /// The event arguments static void ChannelFactory_Closed(object sender, EventArgs e) { (sender as IDisposable).Dispose(); } /// public T GetComponent() { var ToT = typeof(T); if (!ValidInstanceInterfaces.Contains(ToT)) throw new Exception("Invalid type!"); return GetComponentImpl(true); } /// /// Returns the requested component for the instance . This does not guarantee a successful connection. s created this way are recycled for minimum latency and bandwidth usage /// /// The component to retrieve /// If should be used to connect /// The correct component T GetComponentImpl(bool useInstanceName) { if (useInstanceName & InstanceName == null) throw new Exception("Instance not selected!"); var actualToT = typeof(T); var tot = actualToT.Name; if (actualToT == typeof(ITGConnectivity) && !useInstanceName) tot = 'S' + tot; ChannelFactory cf; lock (ChannelFactoryCache) { if (ChannelFactoryCache.ContainsKey(tot)) try { cf = ((ChannelFactory)ChannelFactoryCache[tot]); if (cf.State != CommunicationState.Opened) throw new Exception(); return cf.CreateChannel(); } catch { ChannelFactoryCache[tot].Abort(); ChannelFactoryCache.Remove(tot); } cf = CreateChannel(useInstanceName ? InstanceName : null); ChannelFactoryCache[tot] = cf; } return cf.CreateChannel(); } /// public T GetServiceComponent() { var ToT = typeof(T); if (!ValidServiceInterfaces.Contains(ToT)) throw new Exception("Invalid type!"); return GetComponentImpl(false); } /// /// Directly creates a for without caching. This should be eventually closed by the caller /// /// The component of the channel to be created /// The correct /// Thrown if isn't a valid component ChannelFactory CreateChannel(string instanceName) { var accessPath = instanceName == null ? MasterInterfaceName : String.Format("{0}/{1}", InstanceInterfaceName, instanceName); var InterfaceName = typeof(T).Name; if (!IsRemoteConnection) { var res2 = new ChannelFactory( new NetNamedPipeBinding { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = TransferLimitLocal }, new EndpointAddress(String.Format("net.pipe://localhost/{0}/{1}", accessPath, InterfaceName))); //10 megs res2.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation; return res2; } //okay we're going over var binding = new WSHttpBinding() { SendTimeout = new TimeSpan(0, 0, 40), MaxReceivedMessageSize = TransferLimitRemote }; var requireAuth = InterfaceName != typeof(ITGConnectivity).Name; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; binding.Security.Mode = requireAuth ? SecurityMode.TransportWithMessageCredential : SecurityMode.Transport; //do not require auth for a connectivity check binding.Security.Message.ClientCredentialType = requireAuth ? MessageCredentialType.UserName : MessageCredentialType.None; var address = new EndpointAddress(String.Format("https://{0}:{1}/{2}/{3}", HTTPSURL, HTTPSPort, accessPath, InterfaceName)); var res = new ChannelFactory(binding, address); if (requireAuth) { res.Credentials.UserName.UserName = HTTPSUsername; res.Credentials.UserName.Password = HTTPSPassword; res.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation; } return res; } /// public ConnectivityLevel ConnectionStatus() { return ConnectionStatus(out string unused); } /// public ConnectivityLevel ConnectionStatus(out string error) { try { GetComponentImpl(false).VerifyConnection(); } catch (CommunicationException e) { error = e.ToString(); return ConnectivityLevel.None; } try { GetServiceComponent().Version(); } catch(Exception e) { error = e.ToString(); return ConnectivityLevel.Connected; } try { GetServiceComponent().Version(); error = null; return ConnectivityLevel.Administrator; } catch(Exception e) { error = e.ToString(); return ConnectivityLevel.Authenticated; } } #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) { CloseAllChannels(true); } // 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. // ~Interface() { // // 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 } }