using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using System.ServiceModel;

namespace StarTrinity
{
	

	/// <summary>
	/// gateway from browsers to distributed proxy
	/// </summary>
	class CentralProxy
	{
		//TcpListener browserListener;
		//Thread browserListenerThread;
		bool _disposing;
		readonly CentralProxyMainService _mainService;
		Stream _dpConnection; // can be network stream or ssl stream
        readonly string _dpIpAddress;
		readonly Socket browserListener;
		public int timeSinceLastPing = 0;
		public readonly int _localPortForBrowsers;
		public readonly string _email;
		public CentralProxy(CentralProxyMainService mainService, int minLocalPortForBrowsers, ref int localPortForBrowsers, Stream dpConnection, string email, string dpIpAddress)
		{
            _dpIpAddress = dpIpAddress;
			_email = email;
			_dpConnection = dpConnection;
			_mainService = mainService;			
			browserListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			int retryCount = 0;
		_retry:
			
			try
			{
				WriteToLog("CentralProxy.CentralProxy() opening socket at port {0}", localPortForBrowsers);	
				browserListener.Bind(new IPEndPoint(IPAddress.Any, localPortForBrowsers));
				this._localPortForBrowsers = localPortForBrowsers;
			}
			catch (Exception exc)
			{
				WriteToLog("CentralProxy.CentralProxy() port {0} bind error: {1}", localPortForBrowsers, exc.Message);
				localPortForBrowsers += 1 + new Random().Next(10);
				if (localPortForBrowsers >= 65000) localPortForBrowsers = minLocalPortForBrowsers;
				if (retryCount++ > 20) throw exc;
				goto _retry;
			}
			browserListener.Listen(-1);

			browserListener.BeginAccept(new AsyncCallback(EndAcceptForBrowsers), null);
			WriteToLog("<< CentralProxy.CentralProxy() accepting connections from browsers at port {0}", _localPortForBrowsers);			
		}
		void WriteToLog(string format, params object[] args)
		{
			_mainService.log.Write(null, this.ToString(), format, args);
		}
		void OnException(string objectName, Exception exc)
		{
			_mainService.log.WriteException(null, exc, objectName, null, true);
		}
        public override string ToString()
        {
            return String.Format("cp{0}", _localPortForBrowsers);
        }

		void EndAcceptForBrowsers(IAsyncResult ar)
		{
			try
			{
				Socket browserConnection = browserListener.EndAccept(ar);
				WriteToLog("accepted connection from browser {0}", browserConnection.RemoteEndPoint);
				BeginProcessBrowserConnection(browserConnection);
				browserListener.BeginAccept(new AsyncCallback(EndAcceptForBrowsers), null);
			}
			catch (ObjectDisposedException)
			{
				WriteToLog("stopped accepting connections from browser: socket is disposed");
			}
			catch (Exception exc)
			{
				_mainService.log.WriteException(null, exc, "accepting connection from browser");
				try
				{
					browserListener.BeginAccept(new AsyncCallback(EndAcceptForBrowsers), null);
				}
				catch
				{
				}
			}
		}
		void BeginProcessBrowserConnection(Socket browserConnection)
		{
			WriteToLog(">> BeginProcessBrowserConnection() {0}", browserConnection.RemoteEndPoint);
			// use original code
			//OriginalDp.DoHttpProcessing(new TcpClient() { Client = browserConnection });
			
			var req = new BrowserRequest()
			{
				ConnectionId = Guid.NewGuid().ToString(),
				BrowserEndpoint = browserConnection.RemoteEndPoint				
			};
			req.PendingAction = (dpChannelStream, dpEndpoint) =>
				{
					req.DpEndpoint = dpEndpoint;
					_mainService.log.Write(null, req.ToString(), "creating proxy channel: browser '{0}' - DP {1}",
						browserConnection.RemoteEndPoint, dpEndpoint);										
					ProxyData(req, new TcpClient() { Client = browserConnection }, dpChannelStream, dpEndpoint);					
				};

			// send request to DP (process new browser connection)
			string requestStr = String.Format("connect {0}\r\n", req.ConnectionId);
			byte[] request = Encoding.ASCII.GetBytes(requestStr);
			
			lock (_mainService.pendingRequests)
			{
				_mainService.pendingRequests.Add(req.ConnectionId, req);
			}

			_mainService.log.Write(null, null, "sending request to accept connection from browser: '{0}'", requestStr);
			_dpConnection.Write(request, 0, request.Length);
									
			_mainService.log.Write(null, req.ToString(), "sent request to accept connection from browser: '{0}'. waiting for DP connection..", requestStr);
		}


        #region proxy procedure
        void ProxyData(BrowserRequest req, TcpClient browserTcpClient, Stream dpChannelStream, IPEndPoint dpEndpoint)
		{
			Stream browserStream = browserTcpClient.GetStream();			
			StreamWriter dpChannelStreamWriter = new StreamWriter(dpChannelStream);
			StreamReader dpChannelStreamReader = new StreamReader(dpChannelStream);				
			StreamReader browserStreamReader = new StreamReader(browserStream);
			
			try
			{
				//read the first line HTTP command
                var firstHttpLine = browserStreamReader.ReadLine();
                WriteToLog("proxying request from browser: '{0}'", firstHttpLine);

				if (String.IsNullOrEmpty(firstHttpLine))
                {
                    WriteToLog("rejecting request: empty line");
					browserStreamReader.Close();
					browserStream.Close();
					dpChannelStream.Close();
					return;
				}
				dpChannelStreamWriter.WriteLine(firstHttpLine);

				var splittedFirstHttpLine = firstHttpLine.Split(new [] {' '}, 3);

				String method = splittedFirstHttpLine[0];
				String remoteUri = splittedFirstHttpLine[1];
                if (!_mainService.database.FilterBlockedWebSites(remoteUri))
                {
                    WriteResponse_BlockedSite(browserStream);
                    return;					
				}
                string browserIpAddress = ((IPEndPoint)browserTcpClient.Client.RemoteEndPoint).Address.ToString();
                if (!_mainService.database.AuthenticateBrowserByIp(remoteUri, browserIpAddress, _dpIpAddress))
                {
                    WriteResponse302(browserStream, "http://proxy.startrinity.com/relaylogin.php");
					browserStreamReader.Close();
					browserStream.Close();
					dpChannelStream.Close();
					return;
				}
              

                if (method.ToUpper() == "CONNECT")
                {
                    WriteToLog("proxying HTTPS request...");

                    if (remoteUri.StartsWith("proxy.startrinity.com"))
                    {
                        // proxy without DP
                        SslTunnel.Tunnel(firstHttpLine, browserTcpClient, browserStream, browserStreamReader, _mainService.log);
                    }
                    else
                    {
                        // proxy all lines from initial request of browser
                        for (; ; )
                        {
                            var line = browserStreamReader.ReadLine();
                            dpChannelStreamWriter.WriteLine(line);
                            if (String.IsNullOrEmpty(line)) break;
                        }


                        dpChannelStreamWriter.Flush();

                        // start proxying
                        BeginProxyHttpsFromBrowserToDp(browserStream, dpChannelStream);
                        BeginProxyHttpsFromDpToBrowser(dpChannelStream, browserStream);
                    }

                    // don't destroy streams
                    dpChannelStreamWriter = null;
                    dpChannelStream = null;
                    browserStreamReader = null;
                    browserStream = null;
                    return;
                }
                else // not https
                {
                    // add custom header
                    if (remoteUri.StartsWith("proxy.startrinity.com"))
                    {
                        // proxy without DP
                        OriginalDp.DoHttpProcessing(req.ConnectionId, browserTcpClient, browserStream, _mainService.log, 
                            firstHttpLine, 
                            String.Format("X-Proxied-For: {0}", browserIpAddress));
                        return;
                    }

                    //read the request headers from the client and copy them to our request
                    WriteToLog("proxying headers to DP");
                    int contentLen = ProxyRequestHeaders(browserStreamReader, dpChannelStreamWriter);
                    WriteToLog("proxied headers");

                                       
                    WriteToLog("flushing data to DP");
                    dpChannelStreamWriter.Flush();
                    WriteToLog("flushed data to DP");

                    //using the completed request, check our cache
                    if (method.ToUpper() == "POST")
                    {
                        char[] postBuffer = new char[contentLen];
                        int bytesRead;
                        int totalBytesRead = 0;

                        while (totalBytesRead < contentLen && (bytesRead = browserStreamReader.ReadBlock(postBuffer, 0, contentLen)) > 0)
                        {
                            totalBytesRead += bytesRead;
                            dpChannelStreamWriter.Write(postBuffer, 0, bytesRead);
                        }

                        dpChannelStreamWriter.Close();
                    }

                    WriteToLog("reading response from DP");
                    int responseBytesRead;
                    byte[] buffer = new byte[8192];
                    try
                    {
                        while (dpChannelStream.CanRead == true && (responseBytesRead = dpChannelStream.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            WriteToLog("read {0} bytes response from DP", responseBytesRead);
                            browserStream.Write(buffer, 0, responseBytesRead);
                        }
                    }
                    catch (Exception exc)
                    {
                        _mainService.log.WriteException(null, exc, req.ToString(), "proxying", true);
                    }

                    dpChannelStream.Close(); dpChannelStream = null;
                    browserStream.Flush();
                }
			}
			catch (Exception exc)
			{
				_mainService.log.WriteException(null, exc, req.ToString(), "proxying", true);							
			}
			finally
			{
				if (browserStreamReader != null)
					browserStreamReader.Close();
				if (browserStream != null) 
					browserStream.Close();
				if (dpChannelStreamWriter != null)
					dpChannelStreamWriter.Close();
				if (dpChannelStream != null)
					dpChannelStream.Close();
			}		
		}
        void WriteResponse_BlockedSite(Stream browserStream)
        {
            WriteResponse(browserStream,
                "<html><body><h1>StarTrinity Service</h1><h2>Access is denied. This web site is blocked.</h2></body></html>");
        }
        void WriteResponse(Stream browserStream, string body)
        {
            var browserStreamWriter = new StreamWriter(browserStream);
            browserStreamWriter.WriteLine("HTTP/1.0 403 Forbidden");
            browserStreamWriter.WriteLine("Server: StarTrinity");
            browserStreamWriter.WriteLine("Content-Type: text/html");
            browserStreamWriter.WriteLine("Content-Length: " + body.Length);
            browserStreamWriter.WriteLine();
            browserStreamWriter.Write(body);
            browserStreamWriter.Flush();
        }
        void WriteResponse302(Stream browserStream, string url)
        {
            var browserStreamWriter = new StreamWriter(browserStream);
            browserStreamWriter.WriteLine("HTTP/1.0 302 Found");
            browserStreamWriter.WriteLine("Location: " + url);
            browserStreamWriter.WriteLine();
            browserStreamWriter.Flush();
        }
		void BeginProxyHttpsFromBrowserToDp(Stream browserStream, Stream dpStream)
		{
			try
			{
				byte[] buffer = new byte[10000];
				//WriteToLog("reading https request from browser...");
				browserStream.BeginRead(buffer, 0, buffer.Length, (ar) =>
				{
					try
					{
						int numberOfBytes = browserStream.EndRead(ar);
						if (numberOfBytes > 0)
						{
							//WriteToLog("sending to DP: {0}", BitConverter.ToString(buffer, 0, numberOfBytes));
							dpStream.BeginWrite(buffer, 0, numberOfBytes, (ar2) =>
							{
								try
								{
									dpStream.EndWrite(ar2);
									//WriteToLog("sent to DP");
									BeginProxyHttpsFromBrowserToDp(browserStream, dpStream);
								}
								catch (Exception exc)
								{
									_mainService.log.WriteException(null, exc, null, "proxying https", true);
								}
							}, null);
						}
					}
					catch (Exception exc)
					{
						_mainService.log.WriteException(null, exc, null, "proxying https", true);	
					}
				}, null);		
			}
			catch (Exception exc)
			{
				_mainService.log.WriteException(null, exc, null, "proxying https", true);
			}
		}
		void BeginProxyHttpsFromDpToBrowser(Stream dpStream, Stream browserStream)
		{
			try
			{
				//WriteToLog("reading https response from DP...");
				byte[] buffer = new byte[10000];
				dpStream.BeginRead(buffer, 0, buffer.Length, (ar) =>
				{
					try
					{
						int numberOfBytes = dpStream.EndRead(ar);
						if (numberOfBytes > 0)
						{
							//WriteToLog("sending https response to browser: {0}", BitConverter.ToString(buffer, 0, numberOfBytes));
							browserStream.BeginWrite(buffer, 0, numberOfBytes, (ar2) =>
							{
								try
								{
									browserStream.EndWrite(ar2);
									//WriteToLog("sent response to browser");
									BeginProxyHttpsFromDpToBrowser(dpStream, browserStream);
								}
								catch (Exception exc)
								{
									_mainService.log.WriteException(null, exc, null, "proxying https", true);
								}
							}, null);
						}
					}
					catch (Exception exc)
					{
						_mainService.log.WriteException(null, exc, null, "proxying https", true);
					}
				}, null);
			}
			catch (Exception exc)
			{
				_mainService.log.WriteException(null, exc, null, "proxying https", true);
			}
		}
		private static int ProxyRequestHeaders(StreamReader sr, StreamWriter sw)
		{
			String line;
			int contentLength = 0;
			do
			{
				line = sr.ReadLine();
				sw.WriteLine(line);
				if (String.IsNullOrEmpty(line))
					return contentLength;
				var header = line.Split(new string[] { ": " }, 2, StringSplitOptions.None);
				switch (header[0].ToLower())
				{
					case "content-length":
						int.TryParse(header[1], out contentLength);
						break;
				}
			} while (!String.IsNullOrWhiteSpace(line));
			return contentLength;
		}		
		#endregion
		

		/// <summary>
		/// ping received from DP (may be in new connection/Socket)
		/// </summary>
		public void OnReceivedInitializeFromDp(Stream dpConnection)
		{
            WriteToLog(">> CentralProxy.OnReceivedInitializeFromDp()");
			//if (this.dpConnection != null && this.dpConnection.RemoteEndPoint != dpConnection.RemoteEndPoint)
			{
				try
				{
				//	this.dpConnection.Close();
				}
				catch
				{
				}
				this._dpConnection = dpConnection;
			}
		}
		public void Dispose()
		{
			WriteToLog(">> Dispose()");
			_disposing = true;
			try
			{
				if (browserListener != null)
					browserListener.Close();
			}
			catch
			{
			}
			_dpConnection = null;
			try
			{
				if (_dpConnection != null)
					_dpConnection.Close();
			}
			catch
			{
			}
			_dpConnection = null;
		}
	}

	public class BrowserRequest
	{
		public readonly DateTime creationTime = DateTime.Now;
		public string RequestUri { get; set; }
		public string ConnectionId { get; set; }
		public EndPoint DpEndpoint { get; set; }
		public EndPoint BrowserEndpoint { get; set; }
		public TimeSpan Duration
		{
			get
			{
				return DateTime.Now - creationTime;
			}
		}
		public override string ToString()
		{
			return ConnectionId;
		}
		public long bytesProxiedFromBrowser = 0;
		public long bytesProxiedFromWebSite = 0;
		public string Report
		{
			get
			{
				return String.Format("[{0}] from {1} to '{2}' DP: {3} (time: {4} sec) {5} bytes from website, {6} bytes from browser",
						ConnectionId, BrowserEndpoint, RequestUri, DpEndpoint, Duration.TotalSeconds, bytesProxiedFromWebSite, bytesProxiedFromBrowser);
			}
		}
		public Action<Stream, IPEndPoint> PendingAction { get; set; } // DP SSL stream, DP endpoint
	}

}
