using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
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
{
///
/// Main for communicating the
///
public interface IServerInterface : IDisposable
{
///
/// The of the connected
///
Version ServerVersion { get; }
///
/// The name of the current instance in use. Defaults to
///
string InstanceName { get; }
///
/// The for the . Is for local connections
///
RemoteLoginInfo LoginInfo { 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 ServerInterface : IServerInterface
{
///
/// List of s that can be used with
///
public static readonly IList ValidServiceInterfaces = new List { typeof(ITGSService), typeof(ITGInstanceManager), typeof(ITGConnectivity), typeof(ITGLanding) };
///
/// Version of the interface
///
public static readonly Version Version = Assembly.GetExecutingAssembly().GetName().Version;
///
/// 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";
///
/// The
///
Version _serverVersion;
///
public Version ServerVersion { get
{
lock (this)
if (_serverVersion == null)
{
string rawVersion;
//check ITGSService first for compatiblity reasons
try
{
rawVersion = GetServiceComponent().Version();
}
catch
{
rawVersion = GetServiceComponent().Version();
}
var splits = rawVersion.Split(' ');
_serverVersion = new Version(splits[splits.Length - 1].Substring(1));
}
return _serverVersion;
} }
///
public string InstanceName { get; private set; }
///
/// Backing field for
///
readonly RemoteLoginInfo _loginInfo;
///
public RemoteLoginInfo LoginInfo { get { return _loginInfo; } }
///
/// 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 ServerInterface() { }
///
/// Construct an for a remote connection
///
/// The for a remote connection
public ServerInterface(RemoteLoginInfo loginInfo)
{
if (!loginInfo.HasPassword)
throw new InvalidOperationException("password must be set on loginInfo!");
_loginInfo = loginInfo;
}
///
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 LoginInfo != 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.Close();
}
catch
{
cf.Abort();
}
toRemove.Add(I.Key);
}
foreach (var I in toRemove)
ChannelFactoryCache.Remove(I);
}
}
///
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;
}
///
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 instance to connect to, if any
/// The correct
ChannelFactory CreateChannel(string instanceName)
{
var accessPath = instanceName == null ? MasterInterfaceName : String.Format("{0}/{1}", 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 = 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)
{
//okay we're going over
var binding = new BasicHttpsBinding()
{
SendTimeout = new TimeSpan(0, 0, 40),
MaxReceivedMessageSize = 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
{
GetComponentImpl(false).VerifyConnection();
}
catch (Exception 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
}
}