using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Net; using System.Net.Security; using System.Reflection; using System.Security.Principal; using System.ServiceModel; using System.ServiceModel.Description; using TGS.Interface.Components; namespace TGS.Interface { /// sealed public class Client : IClient { /// /// Version of the /// public static readonly Version Version = Assembly.GetExecutingAssembly().GetName().Version; /// public IServer Server => server; /// public Version ServerVersion { get { lock (this) if (_serverVersion == null) { string rawVersion; //check ITGSService first for compatiblity reasons try { rawVersion = GetComponent(null).Version(); } catch { rawVersion = GetComponent(null).Version(); } var splits = rawVersion.Split(' '); _serverVersion = new Version(splits[splits.Length - 1].Substring(1)); } return _serverVersion; } } /// public string InstanceName { get; private set; } /// public RemoteLoginInfo LoginInfo { get { return _loginInfo; } } /// /// Backing field for /// readonly IServer server; /// /// Backing field for /// readonly RemoteLoginInfo _loginInfo; /// /// The /// Version _serverVersion; /// /// 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; /// /// 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 Client() { ChannelFactoryCache = new Dictionary(); server = new Server(this); } /// /// Construct an for a remote connection /// /// The for a remote connection public Client(RemoteLoginInfo loginInfo) : this() { if (!loginInfo.HasPassword) throw new InvalidOperationException("password must be set on loginInfo!"); _loginInfo = loginInfo; } /// public bool IsRemoteConnection { get { return LoginInfo != null; } } /// /// Closes all s stored in and it /// public void Dispose() { lock (this) { if (ChannelFactoryCache == null) return; foreach (var I in ChannelFactoryCache) { var cf = I.Value; try { cf.Close(); } catch { cf.Abort(); } } ChannelFactoryCache = null; } } /// public bool VersionMismatch(out string errorMessage) { if (ServerVersion.Major != Version.Major || ServerVersion.Minor != Version.Minor || ServerVersion.Build != Version.Build) //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.", Version, ServerVersion); return true; } errorMessage = null; return false; } /// /// 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 /// The name of the to use /// The correct component internal T GetComponent(string instanceName) { var actualToT = typeof(T); var tot = actualToT.Name; if (instanceName != null) tot = instanceName + tot; ChannelFactory cf; lock (this) { if (ChannelFactoryCache == null) throw new ObjectDisposedException(GetType().Name); 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(instanceName); ChannelFactoryCache[tot] = cf; } return cf.CreateChannel(); } /// /// Directly creates a for without caching. This should be eventually closed by the caller /// /// The component of the channel to be created /// The instance to connect to, if any /// The correct ChannelFactory CreateChannel(string instanceName) { var accessPath = instanceName == null ? Definitions.MasterInterfaceName : String.Format("{0}/{1}", Definitions.InstanceInterfaceName, instanceName); if (!IsRemoteConnection) return CreateLocalChannel(instanceName, accessPath); return CreateRemoteChannel(instanceName, accessPath); } //NOTE: This needs to be kept seperate from CreateRemoteChannel because not all our client implementations have NetNamedPipeBinding and attempting to call this function on those platforms will throw an exception /// /// Directly creates a for on a local connection without caching. This should be eventually closed by the caller /// /// The component of the channel to be created /// The instance to connect to, if any /// The URL of the interface to connect to /// The correct ChannelFactory CreateLocalChannel(string instanceName, string accessPath) { var interfaceName = typeof(T).Name; var res2 = new ChannelFactory( new NetNamedPipeBinding { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = Definitions.TransferLimitLocal }, new EndpointAddress(String.Format("net.pipe://localhost/{0}/{1}", accessPath, interfaceName))); //10 megs res2.Credentials.Windows.AllowedImpersonationLevel = TokenImpersonationLevel.Impersonation; return res2; } /// /// Directly creates a for on a remote connection without caching. This should be eventually closed by the caller /// /// The component of the channel to be created /// The instance to connect to, if any /// The URL of the interface to connect to /// The correct ChannelFactory CreateRemoteChannel(string instanceName, string accessPath) { //tls memes ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; //okay we're going over var binding = new BasicHttpsBinding() { SendTimeout = new TimeSpan(0, 0, 40), MaxReceivedMessageSize = Definitions.TransferLimitRemote }; var interfaceName = typeof(T).Name; var requireAuth = interfaceName != typeof(ITGConnectivity).Name; var url = String.Format("https://{0}:{1}/{2}/{3}", LoginInfo.IP, LoginInfo.Port, accessPath, interfaceName); var address = new EndpointAddress(url); var res = new ChannelFactory(binding, address); if (requireAuth) { var applicator = new AuthenticationHeaderApplicator(LoginInfo); var t = Type.GetType("Mono.Runtime"); KeyedCollection behaviours; if (t != null) { var prop = res.Endpoint.GetType() .GetTypeInfo() .GetDeclaredProperty("Behaviors"); behaviours = (KeyedCollection)prop .GetValue(res.Endpoint); } else behaviours = res.Endpoint.EndpointBehaviors; behaviours.Add(applicator); } return res; } /// public ConnectivityLevel ConnectionStatus() { return ConnectionStatus(out string unused); } /// public ConnectivityLevel ConnectionStatus(out string error) { try { GetComponent(null).VerifyConnection(); } catch (Exception e) { error = e.ToString(); return ConnectivityLevel.None; } try { GetComponent(null).Version(); } catch(Exception e) { error = e.ToString(); return ConnectivityLevel.Connected; } try { GetComponent(null).Version(); error = null; return ConnectivityLevel.Administrator; } catch(Exception e) { error = e.ToString(); return ConnectivityLevel.Authenticated; } } } }