//
// TlsTest.cs: TLS/SSL Test Program
//
// Author:
//	Sebastien Pouliot  <sebastien@ximian.com>
//
// (C) 2004 Novell (http://www.novell.com)
//

using System;
using System.Collections;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;

using Mono.Security.Protocol.Tls;

public class TlsTest {

	public static void Usage (string message) 
	{
		Console.WriteLine ("{0}tlstest - Copyright (c) 2004 Novell", Environment.NewLine);
		if (message != null) {
			Console.WriteLine ("{0}{1}{0}", Environment.NewLine, message);
		}
		Console.WriteLine ("Usage:");
		Console.WriteLine ("tlstest [protocol] [class] [credentials] [--x:x509 [--x:x509]] [--time] [--show] url [...]");
		Console.WriteLine ("{0}protocol (only applicable when using stream)", Environment.NewLine);
		Console.WriteLine ("\t--any   \tNegotiate protocol [default]");
		Console.WriteLine ("\t--ssl   \tUse SSLv3");
		Console.WriteLine ("\t--ssl2  \tUse SSLv2 - unsupported on Mono");
		Console.WriteLine ("\t--ssl3  \tUse SSLv3");
		Console.WriteLine ("\t--tls   \tUse TLSv1");
		Console.WriteLine ("\t--tls1  \tUse TLSv1");
		Console.WriteLine ("{0}class", Environment.NewLine);
		Console.WriteLine ("\t--stream\tDirectly use the SslClientStream [default]");
		Console.WriteLine ("\t--web   \tUse the WebRequest/WebResponse classes");
		Console.WriteLine ("{0}credentials", Environment.NewLine);
		Console.WriteLine ("\t--basic:username:password:domain\tBasic Authentication");
		Console.WriteLine ("\t--digest:username:password:domain\tDigest Authentication");
		Console.WriteLine ("{0}options", Environment.NewLine);
		Console.WriteLine ("\t--x:x509\tX.509 client certificate (multiple entries allowed");
		Console.WriteLine ("\t--time  \tShow the time required for each page load");
		Console.WriteLine ("\t--show  \tShow the web page content on screen");
		Console.WriteLine ("{0}\turl [...]\tOne, or more, URL to download{0}", Environment.NewLine);
	}

	private static bool show;
	private static bool time;
	private static bool web;
	private static Mono.Security.Protocol.Tls.SecurityProtocolType protocol = Mono.Security.Protocol.Tls.SecurityProtocolType.Default;
	private static X509CertificateCollection certificates = new X509CertificateCollection ();
	private static NetworkCredential basicCred;
	private static NetworkCredential digestCred;

	public static void Main (string[] args) 
	{
		if (args.Length == 0) {
			Usage ("Missing arguments");
			return;
		}

		ArrayList urls = new ArrayList ();
		foreach (string arg in args) {
			switch (arg) {
				// protocol
				case "--any":
					protocol = Mono.Security.Protocol.Tls.SecurityProtocolType.Default;
					break;
				case "--ssl":
				case "--ssl3":
					protocol = Mono.Security.Protocol.Tls.SecurityProtocolType.Ssl3;
					break;
				case "--ssl2":
					protocol = Mono.Security.Protocol.Tls.SecurityProtocolType.Ssl2;
					// note: will only works with Fx 1.2
					// but the tool doesn't link with it
					Usage ("Not supported");
					return;
				case "--tls":
				case "--tls1":
					protocol = Mono.Security.Protocol.Tls.SecurityProtocolType.Tls;
					break;
				// class
				case "--stream":
					web = false;
					break;
				case "--web":
					web = true;
					break;
				// options
				case "--time":
					time = true;
					break;
				case "--show":
					show = true;
					break;
				case "--help":
					Usage (null);
					return;
				// credentials, certificates, urls or bad options
				default:
					if (arg.StartsWith ("--digest:")) {
						digestCred = GetCredentials (arg.Substring (9));
						continue;
					}
					else if (arg.StartsWith ("--basic:")) {
						basicCred = GetCredentials (arg.Substring (8));
						continue;
					}
					else if (arg.StartsWith ("--x:")) {
						string filename = arg.Substring (4);
						X509Certificate x509 = X509Certificate.CreateFromCertFile (filename);
						certificates.Add (x509);
						continue;
					}
					else if (arg.StartsWith ("--")) {
						Usage ("Invalid option " + arg);
						return;
					}
					urls.Add (arg);
					break;
			}
		}

		if ((web) && (protocol != Mono.Security.Protocol.Tls.SecurityProtocolType.Default)) {
			Usage ("You can't set the protocol when using the WebRequest/WebResponse class");
			return;
		}

		if (urls.Count == 0) {
			Usage ("no URL were specified");
			return;
		}

		foreach (string url in urls) {
			Console.WriteLine ("{0}{1}", Environment.NewLine, url);
			string content = null;
			DateTime start = DateTime.Now;
			
			try {
				if (web) {
					content = GetWebPage (url);
				}
				else {
					content = GetStreamPage (url);
				}
			}
			catch (Exception e) {
				// HResult is protected - but very useful in debugging
				PropertyInfo pi = e.GetType ().GetProperty ("HResult", BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance);
				Console.WriteLine ("FAILED: #{0}", (int)pi.GetValue (e, null));
				Console.WriteLine (e.ToString ());
			}

			TimeSpan ts = (DateTime.Now - start);
			if ((show) && (content != null)) {
				Console.WriteLine ("{0}{1}{0}", Environment.NewLine, content);
			}
			if (time) {
				Console.WriteLine ("Time: " + ts.ToString ());
			}
		}
	}

	public static string GetWebPage (string url) 
	{
		ServicePointManager.CertificatePolicy = new TestCertificatePolicy ();

		Uri uri = new Uri (url);
		HttpWebRequest req = (HttpWebRequest) WebRequest.Create (uri);

		if ((digestCred != null) || (basicCred != null)) {
			CredentialCache cache = new CredentialCache ();
			if (digestCred != null)
				cache.Add (uri, "Digest", digestCred);
			if (basicCred != null)
				cache.Add (uri, "Basic", basicCred);
			req.Credentials = cache;
		}

		if (certificates.Count > 0)
			req.ClientCertificates.AddRange (certificates);
		
		WebResponse resp = req.GetResponse ();
		Stream stream = resp.GetResponseStream ();
		StreamReader sr = new StreamReader (stream, Encoding.UTF8);
		return sr.ReadToEnd ();
	}

	public static string GetStreamPage (string url) 
	{
		Uri uri = new Uri (url);
		if (uri.Scheme != Uri.UriSchemeHttps)
			throw new NotSupportedException ("Stream only works with HTTPS protocol");

		IPHostEntry host = Dns.Resolve (uri.Host);
		IPAddress ip = host.AddressList [0];
		Socket socket = new Socket (ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
		socket.Connect (new IPEndPoint (ip, uri.Port));
		NetworkStream ns = new NetworkStream (socket, false);
		SslClientStream ssl = new SslClientStream (ns, uri.Host, false, protocol, certificates);
		ssl.ServerCertValidationDelegate += new CertificateValidationCallback (CertificateValidation);

		StreamWriter sw = new StreamWriter (ssl);
		sw.WriteLine ("GET {0}{1}", uri.AbsolutePath, Environment.NewLine);
		sw.Flush ();

		StreamReader sr = new StreamReader (ssl, Encoding.UTF8);
		return sr.ReadToEnd ();
	}

	private static NetworkCredential GetCredentials (string credentials) 
	{
		string[] creds = credentials.Split (':');
		NetworkCredential nc = new NetworkCredential ();
		nc.UserName = ((creds.Length > 0) ? creds [0] : String.Empty);
		nc.Password = ((creds.Length > 1) ? creds [1] : String.Empty);
		nc.Domain = ((creds.Length > 2) ? creds [2] : String.Empty);
		return nc;
	}

	private static void ShowCertificateError (int error) 
	{
		string message = null;
		switch (error) {
			case -2146762490:
				message = "CERT_E_PURPOSE 0x800B0106";
				break;
			case -2146762481:
				message = "CERT_E_CN_NO_MATCH 0x800B010F";
				break;
			case -2146869223:
				message = "TRUST_E_BASIC_CONSTRAINTS 0x80096019";
				break;
			case -2146869232:
				message = "TRUST_E_BAD_DIGEST 0x80096010";
				break;
			case -2146762494:
				message = "CERT_E_VALIDITYPERIODNESTING 0x800B0102";
				break;
			case -2146762495:
				message = "CERT_E_EXPIRED 0x800B0101";
				break;
			case -2146762486:
				message = "CERT_E_CHAINING 0x800B010A";
				break;
			case -2146762487:
				message = "CERT_E_UNTRUSTEDROOT 0x800B0109";
				break;
			default:
				message = "unknown (try WinError.h)";
				break;
		}
		Console.WriteLine ("Error #{0}: {1}", error, message);
	}

	private static bool CertificateValidation (X509Certificate certificate, int[] certificateErrors)
	{
		if (certificateErrors.Length > 0) {
			Console.WriteLine (certificate.ToString (true));
			// X509Certificate.ToString(true) doesn't show dates :-(
			Console.WriteLine ("\tValid From:  {0}", certificate.GetEffectiveDateString ());
			Console.WriteLine ("\tValid Until: {0}{1}", certificate.GetExpirationDateString (), Environment.NewLine);
			// multiple errors are possible using SslClientStream
			foreach (int error in certificateErrors) {
				ShowCertificateError (error);
			}
		}
		// whatever the reason we do not stop the SSL connection
		return true;
	}

	public class TestCertificatePolicy : ICertificatePolicy {

		public bool CheckValidationResult (ServicePoint sp, X509Certificate certificate, WebRequest request, int error)
		{
			if (error != 0) {
				Console.WriteLine (certificate.ToString (true));
				// X509Certificate.ToString(true) doesn't show dates :-(
				Console.WriteLine ("\tValid From:  {0}", certificate.GetEffectiveDateString ());
				Console.WriteLine ("\tValid Until: {0}{1}", certificate.GetExpirationDateString (), Environment.NewLine);

				ShowCertificateError (error);
			}
			// whatever the reason we do not stop the SSL connection
			return true;
		}
	}
}

