context: // <airvpn_source_header>
  // This file is part of AirVPN Client software.
  // Copyright (C)2014-2014 AirVPN (support@airvpn.org) / https://airvpn.org )
  //
  // AirVPN Client is free software: you can redistribute it and/or modify
  // it under the terms of the GNU General Public License as published by
  // the Free Software Foundation, either version 3 of the License, or
  // (at your option) any later version.
  // 
  // AirVPN Client is distributed in the hope that it will be useful,
  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  // GNU General Public License for more details.
  // 
  // You should have received a copy of the GNU General Public License
  // along with AirVPN Client. If not, see <http://www.gnu.org/licenses/>.
  // </airvpn_source_header>
  
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.IO;
  using System.Net;
  using System.Net.NetworkInformation;
  using System.Net.Sockets;
  using System.Text;
  using System.Text.RegularExpressions;
  using System.Threading;
  using System.Web;
  using System.Xml;
  using AirVPN.Core;
  
  
  namespace AirVPN.Core.Threads
  {
  	public class Session : AirVPN.Core.Thread
  	{
  		private Process m_processOpenVpn;
  		private Process m_processProxy;
  
  		private Socket m_openVpnManagementSocket;
  		private List<string> m_openVpnManagementCommands = new List<string>();
  		private List<string> m_openVpnManagementStatisticsLines = new List<string>();
  		
  		private string m_reset = "";
  		private int m_proxyPort = 0;
  		private NetworkInterface m_interfaceTun;
  		private int m_timeLastStatus = 0;
  		private TemporaryFile m_fileSshKey;
  		private TemporaryFile m_fileSslCrt;
  		private TemporaryFile m_fileSslConfig;
  		private TemporaryFile m_fileOvpn;
  
  		public override void OnRun()
  		{
  			CancelRequested = false;
  
  			string sessionLastServer = "";
  
  			bool oneConnectionReached = false;
  
  			for (; CancelRequested == false; )
  			{
  				RouteScope routeScope = null;
  
  				bool allowed = true;
  				string waitingMessage = "";
  				int waitingSecs = 0;
  				m_processOpenVpn = null;
  				m_processProxy = null;
  
  				try
  				{
  					// -----------------------------------
  					// Phase 1: Initialization and start
  					// -----------------------------------
  
  					
  					if ((Engine.NextServer == null) && (Pinger.Instance.GetEnabled()) && (Engine.PingerInvalid() != 0))
  					{
  						string lastWaitingMessage = "";
  						for (; ; )
  						{
  							if (CancelRequested)
  								break;
  
  							int i = Engine.PingerInvalid();
  							if (i == 0)
  								break;
  
  							string nextWaitingMessage = Messages.WaitingLatencyTestsTitle + " " + Messages.Format(Messages.WaitingLatencyTestsStep, i.ToString());
  							if (lastWaitingMessage != nextWaitingMessage)
  							{
  								lastWaitingMessage = nextWaitingMessage;
  								Engine.WaitMessageSet(nextWaitingMessage, true);
  							}
  							
  							Sleep(100);
  						}
  					}
  
  					if (CancelRequested)
  						continue;
  
  					m_openVpnManagementCommands.Clear();
  
  					string protocol = Engine.Storage.Get("mode.protocol").ToUpperInvariant();
  					int port = Engine.Storage.GetInt("mode.port");
  					int alt = Engine.Storage.GetInt("mode.alt");
  
  					if (protocol == "AUTO")
  					{						
  						protocol = Engine.Storage.GetManifestKeyValue("mode_protocol", "UDP");
  						port = Conversions.ToInt32(Engine.Storage.GetManifestKeyValue("mode_port", "443"));
  						alt = Conversions.ToInt32(Engine.Storage.GetManifestKeyValue("mode_alt", "0"));
  					}
  
  					if (protocol == "SSH")
  					{
  						m_proxyPort = Engine.Storage.GetInt("ssh.port");
  						if (m_proxyPort == 0)
  							m_proxyPort = RandomGenerator.GetInt(1024, 64 * 1024);
  					}
  					else if (protocol == "SSL")
  					{
  						m_proxyPort = Engine.Storage.GetInt("ssl.port");
  						if (m_proxyPort == 0)
  							m_proxyPort = RandomGenerator.GetInt(1024, 64 * 1024);
  					}
  					else
  					{
  						m_proxyPort = 0;
  					}
  
  					Engine.CurrentServer = Engine.NextServer;
  					Engine.NextServer = null;
  
  					if (Engine.Storage.Get("server") != "")
  					{
  						Engine.CurrentServer = Engine.PickServer(Engine.Storage.Get("server"), true);
  					}
  					else
  					{
  						if (Engine.CurrentServer == null)
  							if (Engine.Storage.GetBool("servers.locklast"))
  								Engine.CurrentServer = Engine.PickServer(sessionLastServer, false);
  
  						if (Engine.CurrentServer == null)
  							Engine.CurrentServer = Engine.PickServer();
  					}
  
  					if (Engine.CurrentServer == null)
  					{
  						allowed = false;
  						Engine.Log(Core.Engine.LogType.Fatal, "No server available.");
  						RequestStop();
  					}
  					
  					// Checking auth user status.
  					// Only to avoid a generic AUTH_FAILED. For that we don't report here for ex. the sshtunnel keys.
  					if (allowed)
  					{
  						Engine.WaitMessageSet(Messages.AuthorizeConnect, true);
  						Engine.Log(Engine.LogType.Info, Messages.AuthorizeConnect);
  
  						Dictionary<string, string> parameters = new Dictionary<string, string>();
  						parameters["act"] = "connect";
  						parameters["server"] = Engine.CurrentServer.Name;
  						parameters["protocol"] = protocol;
  						parameters["port"] = port.ToString();
  						parameters["alt"] = alt.ToString();
  
  						XmlDocument xmlDoc = null;
  						try
  						{
  							xmlDoc = AirExchange.Fetch(Messages.AuthorizeConnect, parameters);
  						}
  						catch (Exception e)
  						{
  							// Note: If failed, continue anyway.
  							Engine.Log(Engine.LogType.Warning, Messages.Format(Messages.AuthorizeConnectFailed, e.Message));
  						}
  
  						if (xmlDoc != null) 
  						{
  							string userMessage = Utils.XmlGetAttributeString(xmlDoc.DocumentElement, "message", "");							
  							if (userMessage != "")
  							{
  								allowed = false;
  								string userMessageAction = Utils.XmlGetAttributeString(xmlDoc.DocumentElement, "message_action", "");								
  								if (userMessageAction == "stop")
  								{
  									Engine.Log(Core.Engine.LogType.Fatal, userMessage);
  									Engine.Disconnect(); // 2.8
  									RequestStop();
  								}
  								else if (userMessageAction == "next")
  								{
  									Engine.CurrentServer.Penality += Engine.Storage.GetInt("advanced.penality_on_error");
  									waitingMessage = userMessage + ", next in {1} sec.";
  									waitingSecs = 5;
  								}
  								else 
  								{
  									waitingMessage = userMessage + ", retry in {1} sec.";
  									waitingSecs = 10;
  								}
  							}
  						}
  					}
  
  					if (CancelRequested)
  						continue;
  
  					if (allowed)
  					{
  						sessionLastServer = Engine.CurrentServer.Name;
  						Engine.Storage.Set("servers.last", Engine.CurrentServer.Name);
  
  
  						if (Engine.CurrentServer.ServerType == 2)
  						{
  							// Routing servers have only 443-UDP-1ip
  							protocol = "UDP";
  							port = 443;
  							alt = 0;
  						}
  
  						routeScope = new RouteScope(Engine.ConnectedEntryIP);
  
  						Engine.RunEventCommand("vpn.pre");
  
  						string connectingMessage = Messages.Format(Messages.ConnectionConnecting, Engine.CurrentServer.PublicName, Engine.CurrentServer.CountryName, Engine.CurrentServer.Location);
  						Engine.WaitMessageSet(connectingMessage, true);
  						Engine.Log(Engine.LogType.InfoImportant, connectingMessage);
  
  						Engine.BuildOVPN(protocol, port, alt, m_proxyPort);
  
  						if (protocol == "SSH")
  						{
  							StartSshProcess();
  						}
  						else if (protocol == "SSL")
  						{
  							StartSslProcess();
  						}
  						else if ((protocol == "TCP") || (protocol == "UDP") || (protocol == "TOR"))
  						{
  							StartOpenVpnProcess();
  						}
  
  						int waitingSleep = 100; // To avoid CPU stress
  
  						m_reset = "";
  
  						// -----------------------------------
  						// Phase 2: Waiting connection
  						// -----------------------------------
  
  						for (; ; )
  						{
  							if( (m_processOpenVpn != null) && (m_processOpenVpn.ReallyExited) ) // 2.2
  								m_reset = "ERROR";
  							if( (m_processProxy != null) && (m_processProxy.ReallyExited) ) // 2.2
  								m_reset = "ERROR";
  
  							if (Engine.IsConnected())
  								break;
  
  							if (m_reset != "")
  								break;
  
  							Sleep(waitingSleep);
  						}
  
  
  						if(m_reset == "")
  							oneConnectionReached = true;
  
  						if( (m_reset == "") && (Engine.Instance.Storage.GetBool("advanced.testmode")) )
  						{
  							m_reset = "STOP";
  						}
  
  						// -----------------------------------
  						// Phase 3 - Running
  						// -----------------------------------
  
  						if (m_reset == "")
  						{
  							for (; ; )
  							{
  								int timeNow = Utils.UnixTimeStamp();
  
  								if (Engine.IsConnected() == false)
  									throw new Exception("Unexpected.");
  
  								ProcessOpenVpnManagement();
  
  								if (timeNow - m_timeLastStatus >= 1)
  								{
  									m_timeLastStatus = timeNow;
  																		
  									// Update traffic stats
  									if (Storage.Simulate)
  									{
  										Engine.Instance.Stats.Charts.Hit(15354, 2525);
  
  										Engine.OnRefreshUi(Core.Engine.RefreshUiMode.Stats);
  									}
  									else if(Platform.Instance.GetTunStatsMode() == "NetworkInterface")
  									{
  										if (m_interfaceTun != null)
  										{
  											Int64 read = m_interfaceTun.GetIPv4Statistics().BytesReceived;
  											Int64 write = m_interfaceTun.GetIPv4Statistics().BytesSent;
  
  											if (Engine.ConnectedLastRead != -1)
  											{
  												int delta = Engine.ConnectedLastStatsTick.Reset();
  												if (delta > 0)
  												{
  													Engine.ConnectedLastDownloadStep = (1000 * (read - Engine.ConnectedLastRead)) / delta;
  													Engine.ConnectedLastUploadStep = (1000 * (write - Engine.ConnectedLastWrite)) / delta;
  												}
  											}
  
  											Engine.ConnectedLastRead = read;
  											Engine.ConnectedLastWrite = write;
  
  											Engine.Instance.Stats.Charts.Hit(Engine.ConnectedLastDownloadStep, Engine.ConnectedLastUploadStep);
  
  											Engine.OnRefreshUi(Core.Engine.RefreshUiMode.Stats);
  										}
  									}
  									else if (Platform.Instance.GetTunStatsMode() == "OpenVpnManagement")
  									{
  										SendManagementCommand("status");
  									}
  								}
  																
  								// Need stop?
  								bool StopRequest = false;
  
  								if (m_reset == "RETRY")
  								{
  									StopRequest = true;
  								}
  
  								if (m_reset == "ERROR")
  								{
  									Engine.CurrentServer.Penality += Engine.Storage.GetInt("advanced.penality_on_error");
  									StopRequest = true;
  								}
  
  								if (Engine.NextServer != null)
  								{
  									StopRequest = true;
  								}
  
  								if (Engine.SwitchServer != false)
  								{
  									Engine.SwitchServer = false;
  									StopRequest = true;
  								}
  
  								if (CancelRequested)
  									StopRequest = true;
  
  								if (StopRequest)
  									break;
  
  								Sleep(waitingSleep);
  							}
  						}
  
  						// -----------------------------------
  						// Phase 4 - Start disconnection
  						// -----------------------------------
  
  						Engine.SetConnected(false);
  
  						Engine.WaitMessageSet(Messages.ConnectionDisconnecting, false);
  						Engine.Log(Engine.LogType.InfoImportant, Messages.ConnectionDisconnecting);
  
  						if (Storage.Simulate)
  						{
  							if (m_processOpenVpn.ReallyExited == false)
  								m_processOpenVpn.Kill();
  						}
  
  						// -----------------------------------
  						// Phase 5 - Waiting disconnection
  						// -----------------------------------
  
  						TimeDelta DeltaSigTerm = null;
  
  						for (; ; )
  						{
  							try
  							{
  								// As explained here: http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/
  								// there isn't any .Net/Mono clean method to send a signal term to a Windows console-only application. So a brutal Kill is performed when there isn't any alternative.
  								// TODO: Maybe optimized under Linux.
  
  								// Simulation process
  								if ((Storage.Simulate) && (m_processOpenVpn != null) && (m_processOpenVpn.ReallyExited == false))
  									m_processOpenVpn.Kill();
  
  								// OpenVPN process completed, but management socket still opened. Strange, but happen. Closing socket.
  								if ((m_processOpenVpn != null) && (m_openVpnManagementSocket != null) && (m_processOpenVpn.ReallyExited == true) && (m_openVpnManagementSocket.Connected))
  									m_openVpnManagementSocket.Close();
  
  								// OpenVPN process still exists, but management socket is not connected. We can't tell to OpenVPN to do a plain disconnection, force killing.
  								if ((m_processOpenVpn != null) && (m_processOpenVpn.ReallyExited == false))
  								{
  									if ((m_openVpnManagementSocket == null) || (m_openVpnManagementSocket.Connected == false))
  										m_processOpenVpn.Kill();
  								}
  
  								// Proxy (SSH/SSL) process								
  								if ((m_processProxy != null) && (m_processOpenVpn != null) && (m_processProxy.ReallyExited == false) && (m_processOpenVpn.ReallyExited == true))
  								{
  									m_processProxy.Kill();
  								}
  
  								// Start a clean disconnection
  								if ((m_processOpenVpn != null) && (m_openVpnManagementSocket != null) && (m_processOpenVpn.ReallyExited == false) && (m_openVpnManagementSocket.Connected))
  								{
  									bool sendSignal = false;
  									if (DeltaSigTerm == null)
  									{
  										DeltaSigTerm = new TimeDelta();
  										sendSignal = true;
  									}
  									else if (DeltaSigTerm.Elapsed(10000)) // Try a SIGTERM every 10 seconds
  										sendSignal = true;
  
  									if(sendSignal)
  									{
  										SendManagementCommand("signal SIGTERM");
  										ProcessOpenVpnManagement();
  									}
  								}
  							}
  							catch (Exception e)
  							{
  								Engine.Log(Core.Engine.LogType.Warning, e);
  							}
  
  							bool exit = true;
  
  							if ((m_openVpnManagementSocket != null) && (m_openVpnManagementSocket.Connected))
  								exit = false;
  
  							if ((m_processProxy != null) && (m_processProxy.ReallyExited == false))
  								exit = false;
  
  							if ((m_processOpenVpn != null) && (m_processOpenVpn.ReallyExited == false))
  								exit = false;
  
  							if (exit)
  								break;
  
  							Sleep(waitingSleep);
  						}
  
  						// -----------------------------------
  						// Phase 6: Cleaning, waiting before retry.
  						// -----------------------------------
  
  						Engine.Log(Engine.LogType.Verbose, Messages.ConnectionStop);
  
  						Engine.RunEventCommand("vpn.down");
  
  						Platform.Instance.OnRouteDefaultRemoveRestore();
  
  						Platform.Instance.OnDnsSwitchRestore();
  
  						// Closing temporary files
  						if (m_fileSshKey != null)
  							m_fileSshKey.Close();
  						if (m_fileSslCrt != null)
  							m_fileSslCrt.Close();
  						if (m_fileSslConfig != null)
  							m_fileSslConfig.Close();
  						if (m_fileOvpn != null)
  							m_fileOvpn.Close();
  					}
  
  					
  
  				}
  				catch (Exception e)
  				{
  					// Warning: Avoid to reach this catch: unpredicable status of running processes.
  					Engine.SetConnected(false);
  
  					Engine.Log(Core.Engine.LogType.Warning, e);					
  				}
  
  				if (routeScope != null)
  					routeScope.End();
  
  				if (m_reset == "AUTH_FAILED")
  				{
  					waitingMessage = "Auth failed, retry in {1} sec.";
  					waitingSecs = 10;
  				}
  				else if (m_reset == "ERROR")
  				{
  					waitingMessage = "Restart in {1} sec.";
  					waitingSecs = 3;
  				}
  				
  				if (Engine.Instance.Storage.GetBool("advanced.testmode"))
  				{
  					Engine.Instance.RequestStop();
  					break;
  				}
  
  				if (waitingSecs > 0)
  				{
  					for (int i = 0; i < waitingSecs; i++)
  					{
  						Engine.WaitMessageSet(Messages.Format(waitingMessage, (waitingSecs - i).ToString()), true);
  						//Engine.Log(Engine.LogType.Verbose, waitingMessage);
  						if (CancelRequested)
  							break;
  
  						Sleep(1000);
  					}
  				}
  			}
  
  			if (oneConnectionReached == false)
  			{
  				if (CancelRequested)
  				{
  					Engine.Log(Engine.LogType.Info, Messages.SessionCancel);
  				}
  				else
  				{
  					Engine.Log(Engine.LogType.Error, Messages.SessionFailed);
  				}
  			}
  			
  			Engine.Instance.WaitMessageClear();
  			
  			Engine.CurrentServer = null; 
  		}
  
  		private void StartSshProcess()
  		{
  			if (m_processProxy != null) // Unexpected
  				return;
  
  			string fileKeyExtension = "";
  			if (Platform.Instance.IsUnixSystem())
  				fileKeyExtension = "key";
  			else
  				fileKeyExtension = "ppk";
  			
  
  			m_fileSshKey = new TemporaryFile(fileKeyExtension);
  			File.WriteAllText(m_fileSshKey.Path, Utils.XmlGetAttributeString(Engine.Storage.User, "ssh_" + fileKeyExtension, ""));
  			
  			if (Platform.Instance.IsUnixSystem())
  			{
  				// TOCHECK: under OSX with chmod 700 fail, need investigation.
  				if (Platform.Instance.GetCode() != "OSX") 
  				{
  					Platform.Instance.ShellCmd("chmod 700 \"" + m_fileSshKey.Path + "\"");
  				}
  			}
  			
  			string arguments = "";
  
  			arguments += " -i \"" + m_fileSshKey.Path + "\" -L " + Conversions.ToString(m_proxyPort) + ":127.0.0.1:2018 sshtunnel@" + Engine.ConnectedEntryIP;
  			if (Platform.Instance.IsUnixSystem())
  				arguments += " -p " + Engine.ConnectedPort; // ssh use -p
  			else
  				arguments += " -P " + Engine.ConnectedPort; // plink use -P			
  				
  			if (Platform.Instance.IsUnixSystem())
  				arguments += " -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"; // TOOPTIMIZE: To bypass key confirmation. Not the best approach.
  
gt: 			arguments += " -N -T -v";