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 inteface class for the service
///
sealed public class Interface : IDisposable
{
///
/// List of s that can be used with and
///
public static readonly IList ValidInterfaces = 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 the communication pipe
/// they are formatted as MasterPipeName/ComponentName
///
public const string MasterInterfaceName = "TGStationServerService";
///
/// If this is set, we will try and connect to an HTTPS server running at this address
///
readonly string HTTPSURL;
///
/// The port used by the service
///
readonly ushort HTTPSPort;
///
/// Username for remote operations
///
readonly string HTTPSUsername;
///
/// Password for remote operations
///
readonly string HTTPSPassword;
///
/// Associated list of open s keyed by type. 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()
{
//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 == typeof(ITGSService).Namespace
&& t.GetCustomAttribute(typeof(ServiceContractAttribute)) != null
select t;
return query.ToList();
}
///
/// 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;
}
///
/// 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 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 certificate failed to verify for {0}:{1}. {2} {3}", HTTPSURL, HTTPSPort, ErrorMessage, cert.ToString());
return handler(ErrorMessage);
};
}
///
/// Closes all s stored in and clears it
///
void ClearCachedChannels()
{
lock (ChannelFactoryCache)
{
foreach (var I in ChannelFactoryCache)
CloseChannel(I.Value);
ChannelFactoryCache.Clear();
}
}
///
/// 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
public bool VersionMismatch(out string errorMessage)
{
var splits = GetComponent().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;
}
///
/// Safely shuts down a single
///
/// The to shutdown
static void CloseChannel(ChannelFactory cf)
{
try
{
cf.Closed += ChannelFactory_Closed;
cf.Close();
}
catch
{
cf.Abort();
}
}
///
/// Disposes a closed
///
/// The channel factory that was closed
/// The event arguments
static void ChannelFactory_Closed(object sender, EventArgs e)
{
(sender as IDisposable).Dispose();
}
///
/// Returns the requested component . This does not guarantee a successful connection. s created this way are recycled for minimum latency and bandwidth usage
///
/// The component to retrieve
/// The correct component
public T GetComponent()
{
var tot = typeof(T);
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();
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 correct
/// Thrown if isn't a valid component
public ChannelFactory CreateChannel()
{
var ToT = typeof(T);
if (!ValidInterfaces.Contains(ToT))
throw new Exception("Invalid type!");
var InterfaceName = typeof(T).Name;
if (HTTPSURL == null)
{
var res2 = new ChannelFactory(
new NetNamedPipeBinding { SendTimeout = new TimeSpan(0, 0, 30), MaxReceivedMessageSize = TransferLimitLocal }, new EndpointAddress(String.Format("net.pipe://localhost/{0}/{1}", MasterInterfaceName, 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, MasterInterfaceName, InterfaceName));
var res = new ChannelFactory(binding, address);
if (requireAuth)
{
res.Credentials.UserName.UserName = HTTPSUsername;
res.Credentials.UserName.Password = HTTPSPassword;
}
return res;
}
///
/// Used to test if the service is avaiable on the machine. Note that state can technically change at any time and any call to the service may throw an exception because it failed
///
/// on successful connection, error message on failure
public string VerifyConnection()
{
try
{
GetComponent().VerifyConnection();
return null;
}
catch (Exception e)
{
return e.ToString();
}
}
///
/// Checks if the supplied user's credentials have permission to use the service. Requires a successful prior call to
///
/// if credentials are valid, otherwise
public bool Authenticate()
{
try
{
GetComponent().Version();
return true;
}
catch
{
return false;
}
}
///
/// Checks if the current login can use . Requires a successful prior call to
///
/// if the connection may use , otherwise
public bool AuthenticateAdmin()
{
try
{
GetComponent().GetCurrentAuthorizedGroup();
return true;
}
catch
{
return false;
}
}
#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)
{
ClearCachedChannels();
}
// 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
}
}