
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
/* Transport Security Layer (TLS)
 * Copyright (c) 2003-2004 Carlos Guzman Alvarez
 * 
 * Permission is hereby granted, free of charge, to any person 
 * obtaining a copy of this software and associated documentation 
 * files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, 
 * publish, distribute, sublicense, and/or sell copies of the Software, 
 * and to permit persons to whom the Software is furnished to do so, 
 * subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */

using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

using Mono.Security.Protocol.Tls.Handshake;

namespace Mono.Security.Protocol.Tls
{
	#region Delegates

	public delegate bool CertificateValidationCallback(
		X509Certificate certificate, 
		int[]			certificateErrors);

	public delegate X509Certificate CertificateSelectionCallback(
		X509CertificateCollection	clientCertificates, 
		X509Certificate				serverCertificate, 
		string						targetHost, 
		X509CertificateCollection	serverRequestedCertificates);

	public delegate AsymmetricAlgorithm PrivateKeySelectionCallback(
		X509Certificate	certificate, 
		string			targetHost);

	#endregion

	public class SslClientStream : Stream, IDisposable
	{
		#region Internal Events
		
		internal event CertificateValidationCallback	ServerCertValidation;
		internal event CertificateSelectionCallback		ClientCertSelection;
		internal event PrivateKeySelectionCallback		PrivateKeySelection;
		
		#endregion

		#region Fields

		private Stream							innerStream;
		private BufferedStream					inputBuffer;
		private ClientContext					context;
		private ClientRecordProtocol			protocol;
		private bool							ownsStream;
		private bool							disposed;
		private bool							checkCertRevocationStatus;
		private object							read;
		private object							write;

		#endregion

		#region Properties

		public override bool CanRead
		{
			get { return this.innerStream.CanRead; }
		}

		public override bool CanSeek
		{
			get { return false; }
		}

		public override bool CanWrite
		{
			get { return this.innerStream.CanWrite; }
		}

		public override long Length
		{
			get { throw new NotSupportedException(); }
		}

		public override long Position
		{
			get { throw new NotSupportedException(); }
			set { throw new NotSupportedException(); }
		}

		// required by HttpsClientStream for proxy support
		internal Stream InputBuffer {
			get { return inputBuffer; }
		}
		#endregion

		#region Security Properties

		public bool CheckCertRevocationStatus 
		{
			get { return this.checkCertRevocationStatus ; }
			set { this.checkCertRevocationStatus = value; }
		}

		public CipherAlgorithmType CipherAlgorithm 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.Cipher.CipherAlgorithmType;
				}

				return CipherAlgorithmType.None;
			}
		}
		
		public int CipherStrength 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.Cipher.EffectiveKeyBits;
				}

				return 0;
			}
		}
		
		public X509CertificateCollection ClientCertificates 
		{
			get { return this.context.ClientSettings.Certificates;}
		}
		
		public HashAlgorithmType HashAlgorithm 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.Cipher.HashAlgorithmType; 
				}

				return HashAlgorithmType.None;
			}
		}
		
		public int HashStrength
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.Cipher.HashSize * 8; 
				}

				return 0;
			}
		}
		
		public int KeyExchangeStrength 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.ServerSettings.Certificates[0].RSA.KeySize;
				}

				return 0;
			}
		}
		
		public ExchangeAlgorithmType KeyExchangeAlgorithm 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.Cipher.ExchangeAlgorithmType; 
				}

				return ExchangeAlgorithmType.None;
			}
		}
		
		public SecurityProtocolType SecurityProtocol 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					return this.context.SecurityProtocol; 
				}

				return 0;
			}
		}
		
		public X509Certificate SelectedClientCertificate 
		{
			get { return this.context.ClientSettings.ClientCertificate; }
		}

		public X509Certificate ServerCertificate 
		{
			get 
			{ 
				if (this.context.HandshakeState == HandshakeState.Finished)
				{
					if (this.context.ServerSettings.Certificates != null &&
						this.context.ServerSettings.Certificates.Count > 0)
					{
						return new X509Certificate(this.context.ServerSettings.Certificates[0].RawData);
					}
				}

				return null;
			}
		} 

		#endregion

		#region Callback Properties

		public CertificateValidationCallback ServerCertValidationDelegate
		{
			get { return this.ServerCertValidation; }
			set { this.ServerCertValidation = value; }			
		}

		public CertificateSelectionCallback ClientCertSelectionDelegate 
		{
			get { return this.ClientCertSelection; }
			set { this.ClientCertSelection = value; }
		}

		public PrivateKeySelectionCallback PrivateKeyCertSelectionDelegate 
		{
			get { return this.PrivateKeySelection; }
			set { this.PrivateKeySelection = value; }
		}

		#endregion

		#region Constructors
		
		public SslClientStream(
			Stream	stream, 
			string	targetHost, 
			bool	ownsStream) 
			: this(
				stream, targetHost, ownsStream, 
				SecurityProtocolType.Default, null)
		{
		}
		
		public SslClientStream(
			Stream				stream, 
			string				targetHost, 
			X509Certificate		clientCertificate) 
			: this(
				stream, targetHost, false, SecurityProtocolType.Default, 
				new X509CertificateCollection(new X509Certificate[]{clientCertificate}))
		{
		}

		public SslClientStream(
			Stream						stream,
			string						targetHost, 
			X509CertificateCollection clientCertificates) : 
			this(
				stream, targetHost, false, SecurityProtocolType.Default, 
				clientCertificates)
		{
		}

		public SslClientStream(
			Stream					stream,
			string					targetHost,
			bool					ownsStream,
			SecurityProtocolType	securityProtocolType) 
			: this(
				stream, targetHost, ownsStream, securityProtocolType,
				new X509CertificateCollection())
		{
		}

		public SslClientStream(
			Stream						stream,
			string						targetHost,
			bool						ownsStream,
			SecurityProtocolType		securityProtocolType,
			X509CertificateCollection	clientCertificates)
		{
			if (stream == null)
			{
				throw new ArgumentNullException("stream is null.");
			}
			if (!stream.CanRead || !stream.CanWrite)
			{
				throw new ArgumentNullException("stream is not both readable and writable.");
			}
			if (targetHost == null || targetHost.Length == 0)
			{
				throw new ArgumentNullException("targetHost is null or an empty string.");
			}

			this.context = new ClientContext(
				this,
				securityProtocolType, 
				targetHost, 
				clientCertificates);

			this.inputBuffer	= new BufferedStream(new MemoryStream());
			this.innerStream	= stream;
			this.ownsStream		= ownsStream;
			this.read			= new object ();
			this.write			= new object ();
			this.protocol		= new ClientRecordProtocol(innerStream, context);
		}

		#endregion

		#region Finalizer

		~SslClientStream()
		{
			this.Dispose(false);
		}

		#endregion

        #region IDisposable Methods

		void IDisposable.Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (!this.disposed)
			{
				if (disposing)
				{
					if (this.innerStream != null)
					{
						if (this.context.HandshakeState == HandshakeState.Finished &&
							!this.context.ConnectionEnd)
						{
							// Write close notify							
							this.protocol.SendAlert(AlertDescription.CloseNotify);
						}

						if (this.ownsStream)
						{
							// Close inner stream
							this.innerStream.Close();
						}
					}
					this.ownsStream				= false;
					this.innerStream			= null;
					this.ClientCertSelection	= null;
					this.ServerCertValidation	= null;
					this.PrivateKeySelection	= null;
				}

				this.disposed = true;
			}
		}

		#endregion

		#region Methods

		public override IAsyncResult BeginRead(
			byte[]			buffer,
			int				offset,
			int				count,
			AsyncCallback	callback,
			object			state)
		{
			this.checkDisposed();
			
			if (buffer == null)
			{
				throw new ArgumentNullException("buffer is a null reference.");
			}
			if (offset < 0)
			{
				throw new ArgumentOutOfRangeException("offset is less than 0.");
			}
			if (offset > buffer.Length)
			{
				throw new ArgumentOutOfRangeException("offset is greater than the length of buffer.");
			}
			if (count < 0)
			{
				throw new ArgumentOutOfRangeException("count is less than 0.");
			}
			if (count > (buffer.Length - offset))
			{
				throw new ArgumentOutOfRangeException("count is less than the length of buffer minus the value of the offset parameter.");
			}

			lock (this)
			{
				if (this.context.HandshakeState == HandshakeState.None)
				{
					this.NegotiateHandshake();
				}
			}

			IAsyncResult asyncResult;

			lock (this.read)
			{
				try
				{
					// If actual buffer is full readed reset it
					if (this.inputBuffer.Position == this.inputBuffer.Length &&
						this.inputBuffer.Length > 0)
					{
						this.resetBuffer();
					}

					if (!this.context.ConnectionEnd)
					{
						// Check if we have space in the middle buffer
						// if not Read next TLS record and update the inputBuffer
						while ((this.inputBuffer.Length - this.inputBuffer.Position) < count)
						{
							// Read next record and write it into the inputBuffer
							long	position	= this.inputBuffer.Position;					
							byte[]	record		= this.protocol.ReceiveRecord();
					
							if (record != null && record.Length > 0)
							{
								// Write new data to the inputBuffer
								this.inputBuffer.Seek(0, SeekOrigin.End);
								this.inputBuffer.Write(record, 0, record.Length);

								// Restore buffer position
								this.inputBuffer.Seek(position, SeekOrigin.Begin);
							}
							else
							{
								if (record == null)
								{
									break;
								}
							}

							// TODO: Review if we need to check the Length
							// property of the innerStream for other types
							// of streams, to check that there are data available
							// for read
							if (this.innerStream is NetworkStream &&
								!((NetworkStream)this.innerStream).DataAvailable)
							{
								break;
							}
						}
					}

					asyncResult = this.inputBuffer.BeginRead(
						buffer, offset, count, callback, state);
				}
				catch (TlsException ex)
				{
					this.protocol.SendAlert(ex.Alert);
					this.Close();

					throw new IOException("The authentication or decryption has failed.");
				}
				catch (Exception)
				{
					throw new IOException("IO exception during read.");
				}
			}

			return asyncResult;
		}

		public override IAsyncResult BeginWrite(
			byte[]			buffer,
			int				offset,
			int				count,
			AsyncCallback	callback,
			object			state)
		{
			this.checkDisposed();

			if (buffer == null)
			{
				throw new ArgumentNullException("buffer is a null reference.");
			}
			if (offset < 0)
			{
				throw new ArgumentOutOfRangeException("offset is less than 0.");
			}
			if (offset > buffer.Length)
			{
				throw new ArgumentOutOfRangeException("offset is greater than the length of buffer.");
			}
			if (count < 0)
			{
				throw new ArgumentOutOfRangeException("count is less than 0.");
			}
			if (count > (buffer.Length - offset))
			{
				throw new ArgumentOutOfRangeException("count is less than the length of buffer minus the value of the offset parameter.");
			}

			lock (this)
			{
				if (this.context.HandshakeState == HandshakeState.None)
				{
					this.NegotiateHandshake();
				}
			}

			IAsyncResult asyncResult;

			lock (this.write)
			{
				try
				{
					// Send the buffer as a TLS record
					
					byte[] record = this.protocol.EncodeRecord(
						ContentType.ApplicationData, buffer, offset, count);
				
					asyncResult = this.innerStream.BeginWrite(
						record, 0, record.Length, callback, state);
				}
				catch (TlsException ex)
				{
					this.protocol.SendAlert(ex.Alert);
					this.Close();

					throw new IOException("The authentication or decryption has failed.");
				}
				catch (Exception)
				{
					throw new IOException("IO exception during Write.");
				}
			}

			return asyncResult;
		}

		public override int EndRead(IAsyncResult asyncResult)
		{
			this.checkDisposed();

			if (asyncResult == null)
			{
				throw new ArgumentNullException("asyncResult is null or was not obtained by calling BeginRead.");
			}

			return this.inputBuffer.EndRead(asyncResult);
		}

		public override void EndWrite(IAsyncResult asyncResult)
		{
			this.checkDisposed();

			if (asyncResult == null)
			{
				throw new ArgumentNullException("asyncResult is null or was not obtained by calling BeginRead.");
			}

			this.innerStream.EndWrite (asyncResult);
		}

		public override void Close()
		{
			((IDisposable)this).Dispose();
		}

		public override void Flush()
		{
			this.checkDisposed();

			this.innerStream.Flush();
		}

		public int Read(byte[] buffer)
		{
			return this.Read(buffer, 0, buffer.Length);
		}

		public override int Read(byte[] buffer, int offset, int count)
		{
			IAsyncResult res = this.BeginRead(buffer, offset, count, null, null);

			return this.EndRead(res);
		}

		public override long Seek(long offset, SeekOrigin origin)
		{
			throw new NotSupportedException();
		}
		
		public override void SetLength(long value)
		{
			throw new NotSupportedException();
		}

		public void Write(byte[] buffer)
		{
			this.Write(buffer, 0, buffer.Length);
		}

		public override void Write(byte[] buffer, int offset, int count)
		{
			IAsyncResult res = this.BeginWrite (buffer, offset, count, null, null);

			this.EndWrite(res);
		}

		#endregion

		#region Misc Methods

		private void resetBuffer()
		{
			this.inputBuffer.SetLength(0);
			this.inputBuffer.Position = 0;
		}

		private void checkDisposed()
		{
			if (this.disposed)
			{
				throw new ObjectDisposedException("The SslClientStream is closed.");
			}
		}

		#endregion

		#region Handsake Methods

		/*
			Client											Server

			ClientHello                 -------->
															ServerHello
															Certificate*
															ServerKeyExchange*
															CertificateRequest*
										<--------			ServerHelloDone
			Certificate*
			ClientKeyExchange
			CertificateVerify*
			[ChangeCipherSpec]
			Finished                    -------->
															[ChangeCipherSpec]
										<--------           Finished
			Application Data            <------->			Application Data

					Fig. 1 - Message flow for a full handshake		
		*/

		internal void NegotiateHandshake()
		{
			lock (this)
			{
				try
				{
					if (this.context.HandshakeState != HandshakeState.None)
					{
						this.context.Clear();
					}

					// Obtain supported cipher suites
					this.context.SupportedCiphers = CipherSuiteFactory.GetSupportedCiphers(this.context.SecurityProtocol);

					// Send client hello
					this.protocol.SendRecord(HandshakeType.ClientHello);

					// Read server response
					while (this.context.LastHandshakeMsg != HandshakeType.ServerHelloDone)
					{
						// Read next record
						this.protocol.ReceiveRecord();
					}

					// Send client certificate if requested
					if (this.context.ServerSettings.CertificateRequest)
					{
						this.protocol.SendRecord(HandshakeType.Certificate);
					}

					// Send Client Key Exchange
					this.protocol.SendRecord(HandshakeType.ClientKeyExchange);

					// Now initialize session cipher with the generated keys
					this.context.Cipher.InitializeCipher();

					// Send certificate verify if requested
					if (this.context.ServerSettings.CertificateRequest)
					{
						this.protocol.SendRecord(HandshakeType.CertificateVerify);
					}

					// Send Cipher Spec protocol
					this.protocol.SendChangeCipherSpec();			
			
					// Read record until server finished is received
					while (this.context.HandshakeState != HandshakeState.Finished)
					{
						// If all goes well this will process messages:
						// 		Change Cipher Spec
						//		Server finished
						this.protocol.ReceiveRecord();
					}

					// Clear Key Info
					this.context.ClearKeyInfo();
				}
				catch (TlsException ex)
				{
					this.protocol.SendAlert(ex.Alert);
					this.Close();

					throw new IOException("The authentication or decryption has failed.");
				}
				catch (Exception)
				{
					this.protocol.SendAlert(AlertDescription.InternalError);
					this.Close();

					throw new IOException("The authentication or decryption has failed.");
				}
			}
		}

		#endregion

		#region Event Methods

		internal virtual bool RaiseServerCertificateValidation(
			X509Certificate certificate, 
			int[]			certificateErrors)
		{
			if (this.ServerCertValidation != null)
			{
				return this.ServerCertValidation(certificate, certificateErrors);
			}

			return (certificateErrors != null && certificateErrors.Length == 0);
		}

		internal X509Certificate RaiseClientCertificateSelection(
			X509CertificateCollection	clientCertificates, 
			X509Certificate				serverCertificate, 
			string						targetHost, 
			X509CertificateCollection	serverRequestedCertificates)
		{
			if (this.ClientCertSelection != null)
			{
				return this.ClientCertSelection(
					clientCertificates,
					serverCertificate,
					targetHost,
					serverRequestedCertificates);
			}

			return null;
		}

		internal AsymmetricAlgorithm RaisePrivateKeySelection(
			X509Certificate certificate, 
			string			targetHost)
		{
			if (this.PrivateKeySelection != null)
			{
				return this.PrivateKeySelection(certificate, targetHost);
			}

			return null;
		}

		#endregion
	}
}
