//******************************************************************************************************
// ServiceClientBase.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 08/20/2010 - J. Ritchie Carroll
// Generated original version of source code.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.ServiceProcess;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using GSF.Communication;
using GSF.Configuration;
using GSF.Console;
using GSF.ErrorManagement;
using GSF.Identity;
using GSF.IO;
using GSF.Net.Security;
using GSF.Reflection;
using GSF.Security;
using GSF.ServiceProcess;
using GSF.Threading;
namespace GSF.TimeSeries
{
///
/// Represents a client that can remotely access the time-series framework service host.
///
public class ServiceClientBase : IDisposable
{
#region [ Members ]
// Fields
private bool m_telnetActive;
private volatile bool m_authenticated;
private volatile bool m_authenticationFailure;
private readonly object m_displayLock;
private readonly ConsoleColor m_originalBgColor;
private readonly ConsoleColor m_originalFgColor;
private ClientBase m_remotingClient;
private ClientHelper m_clientHelper;
private ErrorLogger m_errorLogger;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
public ServiceClientBase()
{
Initialize();
// Save the color scheme.
m_originalBgColor = System.Console.BackgroundColor;
m_originalFgColor = System.Console.ForegroundColor;
m_displayLock = new object();
// Register event handlers.
m_clientHelper.AuthenticationSuccess += ClientHelper_AuthenticationSuccess;
m_clientHelper.AuthenticationFailure += ClientHelper_AuthenticationFailure;
m_clientHelper.ReceivedServiceUpdate += ClientHelper_ReceivedServiceUpdate;
m_clientHelper.ReceivedServiceResponse += ClientHelper_ReceivedServiceResponse;
m_clientHelper.TelnetSessionEstablished += ClientHelper_TelnetSessionEstablished;
m_clientHelper.TelnetSessionTerminated += ClientHelper_TelnetSessionTerminated;
}
///
/// Releases the unmanaged resources before the object is reclaimed by .
///
~ServiceClientBase()
{
Dispose(false);
}
#endregion
#region [ Methods ]
///
/// Initializes the remoting client, client helper, and error logger.
///
public void Initialize()
{
CategorizedSettingsElementCollection remotingClientSettings;
string[] args;
string filter;
try
{
remotingClientSettings = ConfigurationFile.Current.Settings["remotingClient"];
}
catch
{
remotingClientSettings = null;
}
if ((object)remotingClientSettings != null)
{
if (remotingClientSettings.Cast().Any(element => element.Name.Equals("EnabledSslProtocols", StringComparison.OrdinalIgnoreCase) && !element.Value.Equals("None", StringComparison.OrdinalIgnoreCase)))
m_remotingClient = InitializeTlsClient();
else
m_remotingClient = InitializeTcpClient();
}
else
{
m_remotingClient = InitializeTlsClient();
}
args = Arguments.ToArgs(Environment.CommandLine);
filter = Enumerable.Range(0, args.Length)
.Where(index => args[index].StartsWith("--filter=", StringComparison.OrdinalIgnoreCase))
.Select(index => Regex.Replace(args[index], "^--filter=", "", RegexOptions.IgnoreCase))
.FirstOrDefault() ?? ClientHelper.DefaultStatusMessageFilter;
m_clientHelper = new ClientHelper();
m_clientHelper.PersistSettings = true;
m_clientHelper.RemotingClient = m_remotingClient;
m_clientHelper.StatusMessageFilter = filter;
m_clientHelper.Initialize();
m_errorLogger = new ErrorLogger();
m_errorLogger.ErrorLog.FileName = "ServiceClient.ErrorLog.txt";
m_errorLogger.LogToEventLog = false;
m_errorLogger.LogToUI = true;
m_errorLogger.PersistSettings = true;
m_errorLogger.ErrorLog.Initialize();
m_errorLogger.Initialize();
}
///
/// Handles service start event.
///
/// Service start arguments.
public virtual void Start(string[] args)
{
string userInput = string.Empty;
Arguments arguments = new Arguments(string.Join(" ", Arguments.ToArgs(Environment.CommandLine).Where(arg => !arg.StartsWith("--filter=", StringComparison.OrdinalIgnoreCase)).Skip(1)));
if (arguments.Exists("OrderedArg1") && arguments.Exists("restart"))
{
string serviceName = arguments["OrderedArg1"];
if (GSF.Common.IsPosixEnvironment)
{
string serviceCommand = FilePath.GetAbsolutePath(serviceName);
try
{
Command.Execute(serviceCommand, "stop");
}
catch (Exception ex)
{
WriteLine("Failed to stop the {0} daemon: {1}\r\n", serviceName, ex.Message);
}
try
{
Command.Execute(serviceCommand, "start");
}
catch (Exception ex)
{
WriteLine("Failed to restart the {0} daemon: {1}\r\n", serviceName, ex.Message);
}
}
else
{
// Attempt to access service controller for the specified Windows service
ServiceController serviceController = ServiceController.GetServices().SingleOrDefault(svc => string.Compare(svc.ServiceName, serviceName, StringComparison.OrdinalIgnoreCase) == 0);
if (serviceController != null)
{
try
{
if (serviceController.Status == ServiceControllerStatus.Running)
{
WriteLine("Attempting to stop the {0} Windows service...", serviceName);
serviceController.Stop();
// Can't wait forever for service to stop, so we time-out after 20 seconds
serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(20.0D));
if (serviceController.Status == ServiceControllerStatus.Stopped)
WriteLine("Successfully stopped the {0} Windows service.", serviceName);
else
WriteLine("Failed to stop the {0} Windows service after trying for 20 seconds...", serviceName);
// Add an extra line for visual separation of service termination status
WriteLine("");
}
}
catch (Exception ex)
{
WriteLine("Failed to stop the {0} Windows service: {1}\r\n", serviceName, ex.Message);
}
}
// If the service failed to stop or it is installed as stand-alone debug application, we try to forcibly stop any remaining running instances
try
{
Process[] instances = Process.GetProcessesByName(serviceName);
if (instances.Length > 0)
{
int total = 0;
WriteLine("Attempting to stop running instances of the {0}...", serviceName);
// Terminate all instances of service running on the local computer
foreach (Process process in instances)
{
process.Kill();
total++;
}
if (total > 0)
WriteLine("Stopped {0} {1} instance{2}.", total, serviceName, total > 1 ? "s" : "");
// Add an extra line for visual separation of process termination status
WriteLine("");
}
}
catch (Exception ex)
{
WriteLine("Failed to terminate running instances of the {0}: {1}\r\n", serviceName, ex.Message);
}
// Attempt to restart Windows service...
if (serviceController != null)
{
try
{
// Refresh state in case service process was forcibly stopped
serviceController.Refresh();
if (serviceController.Status != ServiceControllerStatus.Running)
serviceController.Start();
}
catch (Exception ex)
{
WriteLine("Failed to restart the {0} Windows service: {1}\r\n", serviceName, ex.Message);
}
}
}
}
else
{
if (arguments.Exists("server"))
{
// Override default settings with user provided input.
m_clientHelper.PersistSettings = false;
m_remotingClient.PersistSettings = false;
m_remotingClient.ConnectionString = $"Server={arguments["server"]}";
}
long lastConnectAttempt = 0;
// Connect to service and send commands.
while ((object)userInput != null && !string.Equals(userInput, "Exit", StringComparison.OrdinalIgnoreCase))
{
try
{
// Do not reattempt connection too quickly
while (DateTime.UtcNow.Ticks - lastConnectAttempt < Ticks.PerSecond)
Thread.Sleep(200);
lastConnectAttempt = DateTime.UtcNow.Ticks;
ICancellationToken timeoutCancellationToken = new Threading.CancellationToken();
if (System.Console.IsInputRedirected)
{
// If the client is invoked as part of a command line script,
// implement a 5-second timeout in case of connectivity issues
Action timeoutAction = () =>
{
if (!timeoutCancellationToken.IsCancelled)
Environment.Exit(1);
};
timeoutAction.DelayAndExecute(5000);
}
if (!m_authenticationFailure)
{
// If there has been no authentication
// failure, connect normally
Connect();
}
else
{
UserInfo userInfo;
StringBuilder username = new StringBuilder();
StringBuilder password = new StringBuilder();
// If there has been an authentication failure,
// prompt the user for new credentials
PromptForCredentials(username, password);
try
{
// Attempt to set network credentials used when attempting AD authentication
userInfo = new UserInfo(username.ToString());
userInfo.Initialize();
SetNetworkCredential(new NetworkCredential(userInfo.LoginID, password.ToString()));
}
catch
{
// Even if this fails, we can still pass along default credentials
SetNetworkCredential(null);
}
Connect(username.ToString(), password.ToString());
}
timeoutCancellationToken.Cancel();
while (m_authenticated && m_clientHelper.Enabled && (object)userInput != null && !string.Equals(userInput, "Exit", StringComparison.OrdinalIgnoreCase))
{
// Wait for a command from the user.
userInput = System.Console.ReadLine()?.Trim();
// Write a blank line to the console.
WriteLine();
if (!string.IsNullOrWhiteSpace(userInput))
{
// The user typed in a command and didn't just hit .
switch (userInput.ToUpper())
{
case "CLS":
// User wants to clear the console window.
System.Console.Clear();
break;
case "EXIT":
// User wants to exit the telnet session with the service.
if (m_telnetActive)
{
userInput = string.Empty;
m_clientHelper.SendRequest("Telnet -disconnect");
}
break;
case "LOGIN":
m_authenticated = false;
m_authenticationFailure = true;
break;
default:
// User wants to send a request to the service.
m_clientHelper.SendRequest(userInput);
if (string.Compare(userInput, "Help", StringComparison.OrdinalIgnoreCase) == 0)
DisplayHelp();
break;
}
}
}
m_clientHelper.Disconnect();
}
catch (Exception)
{
// Errors during the outer connection loop
// should simply force an attempt to reconnect
m_clientHelper.Disconnect();
}
}
}
}
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
if ((object)m_clientHelper != null)
{
m_clientHelper.Dispose();
m_clientHelper = null;
}
if ((object)m_remotingClient != null)
{
m_remotingClient.Dispose();
m_remotingClient = null;
}
if ((object)m_errorLogger != null)
{
m_errorLogger.Dispose();
m_errorLogger = null;
}
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
private TcpClient InitializeTcpClient()
{
TcpClient remotingClient;
remotingClient = new TcpClient();
remotingClient.ConnectionString = "Server=localhost:8500";
remotingClient.IgnoreInvalidCredentials = true;
remotingClient.PayloadAware = true;
remotingClient.PersistSettings = true;
remotingClient.SettingsCategory = "RemotingClient";
remotingClient.Initialize();
return remotingClient;
}
private TlsClient InitializeTlsClient()
{
TlsClient remotingClient;
remotingClient = new TlsClient();
remotingClient.ConnectionString = "Server=localhost:8500";
remotingClient.IgnoreInvalidCredentials = true;
remotingClient.PayloadAware = true;
remotingClient.PersistSettings = true;
remotingClient.SettingsCategory = "RemotingClient";
remotingClient.TrustedCertificatesPath = $"Certs{Path.DirectorySeparatorChar}Remotes";
remotingClient.ValidChainFlags = X509ChainStatusFlags.UntrustedRoot;
remotingClient.ValidPolicyErrors = SslPolicyErrors.RemoteCertificateChainErrors;
remotingClient.Initialize();
// Override remote certificate validation so that we always
// accept localhost, but fall back on SimplePolicyChecker.
remotingClient.RemoteCertificateValidationCallback = RemoteCertificateValidationCallback;
return remotingClient;
}
private void Connect()
{
m_authenticated = false;
m_authenticationFailure = false;
m_clientHelper.Connect();
if (m_authenticationFailure)
m_clientHelper.Disconnect();
}
private void Connect(string username, string password)
{
m_clientHelper.Username = username.ToNonNullString();
// If the communications channel is secured or the user
// has entered a blank password, send the password as-is
if (string.IsNullOrEmpty(password) || m_clientHelper.RemotingClient is TlsClient)
{
m_clientHelper.Password = password.ToNonNullString();
Connect();
}
// If the client fails to connect with the as-is password,
// attempt to connect again with an encrypted password
// because the server may only be authenticating against
// the encrypted password
if (!m_clientHelper.RemotingClient.Enabled && !string.IsNullOrEmpty(password))
{
m_clientHelper.Password = SecurityProviderUtility.EncryptPassword(password);
Connect();
}
}
private void PromptForCredentials(StringBuilder username, StringBuilder password)
{
StringBuilder prompt = new StringBuilder();
ConsoleKeyInfo key;
lock (m_displayLock)
{
prompt.AppendLine();
prompt.AppendLine();
prompt.AppendLine("Connection to the service was rejected due to authentication failure.");
prompt.AppendLine("Enter the credentials to be used for authentication with the service.");
prompt.AppendLine();
Write(prompt.ToString());
// Capture the user name.
Write("Enter user name: ");
username.Append(System.Console.ReadLine());
// Capture the password.
Write("Enter password: ");
char endOfLine = GSF.Common.IsPosixEnvironment ? '\n' : '\r';
while ((key = System.Console.ReadKey(true)).KeyChar != endOfLine)
{
switch (key.Key)
{
case ConsoleKey.Backspace:
if (password.Length > 0)
password.Remove(password.Length - 1, 1);
break;
case ConsoleKey.Escape:
password.Clear();
break;
default:
password.Append(key.KeyChar);
break;
}
}
WriteLine();
}
}
private void SetNetworkCredential(NetworkCredential credential)
{
TlsClient tlsClient;
TcpClient tcpClient;
tlsClient = m_remotingClient as TlsClient;
if ((object)tlsClient != null)
{
tlsClient.NetworkCredential = credential;
}
else
{
tcpClient = m_remotingClient as TcpClient;
if ((object)tcpClient != null)
tcpClient.NetworkCredential = credential;
}
}
private bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
TlsClient remotingClient;
IPEndPoint remoteEndPoint;
IPHostEntry localhost;
SimplePolicyChecker policyChecker;
remotingClient = m_remotingClient as TlsClient;
if ((object)remotingClient != null)
{
remoteEndPoint = remotingClient.Client.RemoteEndPoint as IPEndPoint;
if ((object)remoteEndPoint != null)
{
// Create an exception and do not check policy for localhost
localhost = Dns.GetHostEntry("localhost");
if (localhost.AddressList.Any(address => address.Equals(remoteEndPoint.Address)))
return true;
}
// Not connected to localhost, so use the policy checker
policyChecker = new SimplePolicyChecker();
policyChecker.ValidPolicyErrors = remotingClient.ValidPolicyErrors;
policyChecker.ValidChainFlags = remotingClient.ValidChainFlags;
return policyChecker.ValidateRemoteCertificate(sender, certificate, chain, sslPolicyErrors);
}
return false;
}
private void DisplayHelp()
{
StringBuilder help = new StringBuilder();
help.AppendFormat("Commands supported by {0}:", AssemblyInfo.EntryAssembly.Name);
help.AppendLine();
help.AppendLine();
help.Append("Command".PadRight(20));
help.Append(" ");
help.Append("Description".PadRight(55));
help.AppendLine();
help.Append(new string('-', 20));
help.Append(" ");
help.Append(new string('-', 55));
help.AppendLine();
help.Append("Cls".PadRight(20));
help.Append(" ");
help.Append("Clears this console screen".PadRight(55));
help.AppendLine();
help.Append("Login".PadRight(20));
help.Append(" ");
help.Append("Disconnects from server and prompts for credentials".PadRight(55));
help.AppendLine();
help.Append("Exit".PadRight(20));
help.Append(" ");
help.Append("Exits this console screen".PadRight(55));
help.AppendLine();
help.AppendLine();
help.AppendLine();
Write(help.ToString());
}
///
/// Client helper authentication success reception handler.
///
/// Sending object.
/// Event argument.
private void ClientHelper_AuthenticationSuccess(object sender, EventArgs e)
{
m_authenticated = true;
}
///
/// Client helper authentication failure reception handler.
///
/// Sending object.
/// Event argument.
private void ClientHelper_AuthenticationFailure(object sender, CancelEventArgs e)
{
m_authenticationFailure = true;
}
///
/// Client helper service update reception handler.
///
/// Sending object.
/// Event argument containing update type and associated message data.
protected virtual void ClientHelper_ReceivedServiceUpdate(object sender, EventArgs e)
{
lock (m_displayLock)
{
// Output status updates from the service to the console window.
switch (e.Argument1)
{
case UpdateType.Alarm:
System.Console.ForegroundColor = ConsoleColor.Red;
break;
case UpdateType.Information:
System.Console.ForegroundColor = m_originalFgColor;
break;
case UpdateType.Warning:
System.Console.ForegroundColor = ConsoleColor.Yellow;
break;
}
Write(e.Argument2);
System.Console.ForegroundColor = m_originalFgColor;
}
}
///
/// Client helper service response reception handler.
///
/// Sending object.
/// Event argument containing the service response.
protected virtual void ClientHelper_ReceivedServiceResponse(object sender, EventArgs e)
{
string sourceCommand;
bool responseSuccess;
byte[] reportData;
if (ClientHelper.TryParseActionableResponse(e.Argument, out sourceCommand, out responseSuccess))
{
string message = e.Argument.Message;
lock (m_displayLock)
{
if (responseSuccess)
{
if (string.IsNullOrWhiteSpace(message))
Write("{0} command processed successfully.\r\n\r\n", sourceCommand);
else
Write("{0}\r\n\r\n", message);
}
else
{
System.Console.ForegroundColor = ConsoleColor.Red;
if (string.IsNullOrWhiteSpace(message))
Write("{0} failure.\r\n\r\n", sourceCommand);
else
Write("{0} failure: {1}\r\n\r\n", sourceCommand, message);
System.Console.ForegroundColor = m_originalFgColor;
}
}
try
{
// Handle reports coming from service
if (responseSuccess && sourceCommand.Equals("GetReport", StringComparison.OrdinalIgnoreCase))
{
reportData = e.Argument.Attachments[0] as byte[];
if ((object)reportData != null)
{
string tempPath = Path.Combine(Path.GetTempPath(), $"{Process.GetCurrentProcess().Id}.pdf");
File.WriteAllBytes(tempPath, reportData);
using (Process.Start(tempPath))
{
}
}
}
}
catch (Exception ex)
{
lock (m_displayLock)
{
System.Console.ForegroundColor = ConsoleColor.Red;
WriteLine("Unable to display report due to exception: {0}", ex.Message);
System.Console.ForegroundColor = m_originalFgColor;
}
}
}
}
///
/// Client helper telnet session established handler.
///
/// Sending object.
/// Event arguments, if any.
protected virtual void ClientHelper_TelnetSessionEstablished(object sender, EventArgs e)
{
// Change the console color scheme to indicate active telnet session.
m_telnetActive = true;
lock (m_displayLock)
{
System.Console.BackgroundColor = ConsoleColor.Blue;
System.Console.ForegroundColor = ConsoleColor.Gray;
System.Console.Clear();
}
}
///
/// Client helper telnet session terminated handler.
///
/// Sending object.
/// Event arguments, if any.
protected virtual void ClientHelper_TelnetSessionTerminated(object sender, EventArgs e)
{
// Revert to original color scheme to indicate end of telnet session.
m_telnetActive = false;
lock (m_displayLock)
{
System.Console.BackgroundColor = m_originalBgColor;
System.Console.ForegroundColor = m_originalFgColor;
System.Console.Clear();
}
}
private void Write(string format, params object[] args)
{
lock (m_displayLock)
{
if (args.Length == 0)
System.Console.Write(format);
else
System.Console.Write(format, args);
}
}
private void WriteLine()
{
lock (m_displayLock)
{
System.Console.WriteLine();
}
}
private void WriteLine(string format, params object[] args)
{
lock (m_displayLock)
{
if (args.Length == 0)
System.Console.WriteLine(format);
else
System.Console.WriteLine(format, args);
}
}
#endregion
}
}