using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Net.Sockets;
using bareFTP.Preferences;

namespace bareFTP.Protocol.Sftp
{
	

	public class Session
	{
		private static string version="bareFTP-0.0.0";

		internal const int SSH_MSG_DISCONNECT=                      1;
		internal const int SSH_MSG_IGNORE=                          2;
		internal const int SSH_MSG_UNIMPLEMENTED=                   3;
		internal const int SSH_MSG_DEBUG=                           4;
		internal const int SSH_MSG_SERVICE_REQUEST=                 5;
		internal const int SSH_MSG_SERVICE_ACCEPT=                  6;
		internal const int SSH_MSG_KEXINIT=                        20;
		internal const int SSH_MSG_NEWKEYS=                        21;
		internal const int SSH_MSG_KEXDH_INIT=                     30;
		internal const int SSH_MSG_KEXDH_REPLY=                    31;
		internal const int SSH_MSG_KEX_DH_GEX_GROUP=               31;
		internal const int SSH_MSG_KEX_DH_GEX_INIT=                32;
		internal const int SSH_MSG_KEX_DH_GEX_REPLY=               33;
		internal const int SSH_MSG_KEX_DH_GEX_REQUEST=             34;
		internal const int SSH_MSG_USERAUTH_REQUEST=               50;
		internal const int SSH_MSG_USERAUTH_FAILURE=               51;
		internal const int SSH_MSG_USERAUTH_SUCCESS=               52;
		internal const int SSH_MSG_USERAUTH_BANNER=                53;
		internal const int SSH_MSG_USERAUTH_INFO_REQUEST=          60;
		internal const int SSH_MSG_USERAUTH_INFO_RESPONSE=         61;
		internal const int SSH_MSG_USERAUTH_PK_OK=                 60;
		internal const int SSH_MSG_GLOBAL_REQUEST=                 80;
		internal const int SSH_MSG_REQUEST_SUCCESS=                81;
		internal const int SSH_MSG_REQUEST_FAILURE=                82;
		internal const int SSH_MSG_CHANNEL_OPEN=                   90;
		internal const int SSH_MSG_CHANNEL_OPEN_CONFIRMATION=      91;
		internal const int SSH_MSG_CHANNEL_OPEN_FAILURE=           92;
		internal const int SSH_MSG_CHANNEL_WINDOW_ADJUST=          93;
		internal const int SSH_MSG_CHANNEL_DATA=                   94;
		internal const int SSH_MSG_CHANNEL_EXTENDED_DATA=          95;
		internal const int SSH_MSG_CHANNEL_EOF=                    96;
		internal const int SSH_MSG_CHANNEL_CLOSE=                  97;
		internal const int SSH_MSG_CHANNEL_REQUEST=                98;
		internal const int SSH_MSG_CHANNEL_SUCCESS=                99;
		internal const int SSH_MSG_CHANNEL_FAILURE=               100;

		private byte[] V_S;                                 // server version
		private byte[] V_C= System.Text.Encoding.Default.GetBytes("SSH-2.0-"+version); // client version

		private byte[] I_C; // the payload of the client's SSH_MSG_KEXINIT
		private byte[] I_S; // the payload of the server's SSH_MSG_KEXINIT
		//private byte[] K_S; // the host key

		private byte[] session_id;

		private byte[] IVc2s;
		private byte[] IVs2c;
		private byte[] Ec2s;
		private byte[] Es2c;
		private byte[] MACc2s;
		private byte[] MACs2c;

		private int seqi=0;
		private int seqo=0;

		private Cipher s2ccipher;
		private Cipher c2scipher;
		private MAC s2cmac;
		private MAC c2smac;
		private byte[] mac_buf;

		private Compression deflater;
		private Compression inflater;

		private IO io;
		private Socket socket;
		private int timeout=0;

		private bool _isConnected=false;

		private bool isAuthed=false;

		private Thread connectThread=null;

		internal Stream In=null;
		internal Stream Out=null;

		internal static bareFTP.Protocol.Sftp.jce.Random random;

		internal Buffer buf;
		internal Packet packet;
		
		private UserInfo userinfo;

		internal string host="127.0.0.1";
		internal int port=22;

		internal string username=null;
		internal string password=null;
		
		private bareFTP.Protocol.IDialogHost dhost;
		private Config conf;
		
		internal Session(Config conf)  
		{
			this.conf = conf;
			buf=new Buffer();
			packet=new Packet(buf);
		}
				
		public IDialogHost DialogHost
		{
			get { return dhost; }
			set { dhost = value; }
		}
		
		public void connect() 
		{
			connect(timeout);
		}

		public void connect(int connectTimeout) 
		{
			
			if(_isConnected)
			{
				throw new Exception("session is already connected");
			}
			io=new IO();
			if(random==null)
			{
				try
				{
					random = new bareFTP.Protocol.Sftp.jce.Random();
					
				}
				catch(Exception e)
				{
					System.Console.Error.WriteLine("connect: random "+e);
				}
			}
			Packet.setRandom(random);
			
			try	
			{
				int i, j;
				
				Stream In;
				Stream Out;
				
	
				socket=Util.createSocket(host, port, connectTimeout);
				In = new NetworkStream(socket); //;socket.getInputStream();
				Out = new NetworkStream(socket); //;socket.getOutputStream();
				
				//if(timeout>0){ socket.setSoTimeout(timeout); }
				socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, 1);
				//socket.setTcpNoDelay(true);
				
				io.setInputStream(In);
				io.setOutputStream(Out);
				
				if(connectTimeout>0 && socket!=null)
				{
					//socket.setSoTimeout(connectTimeout);
					socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, connectTimeout);
					socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, connectTimeout);
				}

				_isConnected=true;

				while(true)
				{

					i=0;
					j=0;
					while(i<buf.buffer.Length)
					{
						j=io.getByte();
						if(j<0)break;
						buf.buffer[i]=(byte)j; i++;
						if(j==10)break;
					}
					if(j<0)
					{
						throw new Exception("connection is closed by foreign host");
					}

					if(buf.buffer[i-1]==10)
					{    // 0x0a
						i--;
						if(buf.buffer[i-1]==13)
						{  // 0x0d
							i--;
						}
					}

					if(i>4 && (i!=buf.buffer.Length) &&
						(buf.buffer[0]!='S'||buf.buffer[1]!='S'||
						buf.buffer[2]!='H'||buf.buffer[3]!='-'))
					{
						//System.err.println(new String(buf.buffer, 0, i);
						continue;
					}

					if(i==buf.buffer.Length ||
						i<7 ||                                      // SSH-1.99 or SSH-2.0
						(buf.buffer[4]=='1' && buf.buffer[6]!='9')  // SSH-1.5
						)
					{
						throw new Exception("invalid server's version String");
					}
					break;
				}				

				V_S=new byte[i]; Util.arraycopy(buf.buffer, 0, V_S, 0, i);
				//System.Console.WriteLine("V_S: ("+i+") ["+new String(V_S)+"]");

				//io.put(V_C, 0, V_C.Length); io.put("\n".getBytes(), 0, 1);
			{
				// Some Cisco devices will miss to read '\n' if it is sent separately.
				byte[] foo=new byte[V_C.Length+1];
				Util.arraycopy(V_C, 0, foo, 0, V_C.Length);
				foo[foo.Length-1]=(byte)'\n';
				io.put(foo, 0, foo.Length);
			}

				buf=read(buf);
				//System.Console.WriteLine("read: 20 ? "+buf.buffer[5]);
				if(buf.buffer[5]!=SSH_MSG_KEXINIT)
				{
					throw new Exception("invalid protocol: "+buf.buffer[5]);
				}
				KeyExchange kex=receive_kexinit(buf);

				while(true)
				{
					buf=read(buf);
					if(kex.getState()==buf.buffer[5])
					{
						bool result=kex.next(buf);
						if(!result)
						{
							//System.Console.WriteLine("verify: "+result);
							in_kex=false;
							throw new Exception("verify: "+result);
						}
					}
					else
					{
						in_kex=false;
						throw new Exception("invalid protocol(kex): "+buf.buffer[5]);
					}
					if(kex.getState()==KeyExchange.STATE_END)
					{
						break;
					}
				}

				try{ checkHost(host, kex); }
				catch(Exception ee)
				{
					in_kex=false;
					throw ee;
				}

				send_newkeys();

				// receive SSH_MSG_NEWKEYS(21)
				buf=read(buf);
				//System.Console.WriteLine("read: 21 ? "+buf.buffer[5]);
				if(buf.buffer[5]==SSH_MSG_NEWKEYS)
				{
					receive_newkeys(buf, kex);
				}
				else
				{
					in_kex=false;
					throw new Exception("invalid protocol(newkyes): "+buf.buffer[5]);
				}

				bool auth=false;
				bool auth_cancel=false;

				UserAuthNone usn=new UserAuthNone(userinfo);
				auth=usn.start(this);

				string methods=null;


				if(!auth)
				{
					methods=usn.getMethods();
					if(methods!=null)
					{
						methods=methods.ToLower();
					}
					else
					{
						// methods: publickey,password,keyboard-interactive
						methods="password";
					}
				}

				while(true)
				{
					while(!auth && methods!=null && methods.Length > 0)
					{

						UserAuth us = null;
						us=new UserAuthPassword(userinfo);
						if(us!=null)
						{
							try
							{
								auth=us.start(this);
								auth_cancel=false;
							}
							catch(JSchAuthCancelException)
							{
								auth_cancel=true;
							}
							catch(JSchPartialAuthException ee)
							{
								methods=ee.Methods;
								auth_cancel=false;
								continue;
							}
							catch(Exception ee)
							{
								System.Console.WriteLine("ee: "+ee); // SSH_MSG_DISCONNECT: 2 Too many authentication failures
							}
						}
						if(!auth)
						{
							int comma=methods.IndexOf(",");
							if(comma==-1) break;
							methods=methods.Substring(comma+1);
						}
					}
					break;
				}

				if(connectTimeout>0 || timeout>0)
				{
					socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, connectTimeout);
					socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, connectTimeout);
				}

				if(auth)
				{
					isAuthed=true;
					connectThread=new Thread(this.run);
					connectThread.Name = "Connect thread "+host+" session";
					connectThread.Start();
					return;
				}
				if(auth_cancel)
					throw new Exception("Authentication canceled");
				throw new Exception("Authentication failed");
			}
			catch(Exception e) 
			{
				in_kex=false;
				if(_isConnected)
				{
					try
					{
						packet.reset();
						buf.putByte((byte)SSH_MSG_DISCONNECT);
						buf.putInt(3);
						buf.putString(System.Text.Encoding.UTF8.GetBytes(e.ToString()));
						buf.putString(System.Text.Encoding.UTF8.GetBytes("en"));
						write(packet);
						disconnect();
					}
					catch(Exception)
					{
					}
				}
				_isConnected=false;
				if(e is Exception) throw (Exception)e;
				throw new Exception("Session.connect: "+e);
			}
		}

		private KeyExchange receive_kexinit(Buffer buf)  
		{
			int j=buf.getInt();
			if(j!=buf.getLength())
			{    // packet was compressed and
				buf.getByte();     // j is the size of deflated packet.
				I_S=new byte[buf.index-5];
			}
			else
			{
				I_S=new byte[j-1-buf.getByte()];
			}
			Util.arraycopy(buf.buffer, buf.s, I_S, 0, I_S.Length);
			
			send_kexinit();
			string[] guess=KeyExchange.guess(I_S, I_C);
			if(guess==null)
			{
				throw new Exception("Algorithm negotiation fail");
			}

			if(!isAuthed &&
				(guess[KeyExchange.PROPOSAL_ENC_ALGS_CTOS] == "none") ||
				(guess[KeyExchange.PROPOSAL_ENC_ALGS_STOC] == "none"))
			{
				throw new Exception("NONE Cipher should not be chosen before authentification is successed.");
			}

			KeyExchange kex=null;
			try
			{
				kex = (KeyExchange)System.Activator.CreateInstance(System.Type.GetType(conf.getSSHAlgorithm((guess[KeyExchange.PROPOSAL_KEX_ALGS]))));
			}
			catch(Exception e){ System.Console.Error.WriteLine("kex: "+e); }
			kex._guess=guess;
			kex.init(this, V_S, V_C, I_S, I_C);
			return kex;
		}

		private bool in_kex=false;
		public void rekey()  
		{
			send_kexinit();
		}
		private void send_kexinit()  
		{
			if(in_kex) return;
			in_kex=true;

			packet.reset();
			buf.putByte((byte) SSH_MSG_KEXINIT);
			lock(random)
			{
				random.fill(buf.buffer, buf.index, 16); buf.skip(16);
		}

			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("kex")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("server_host_key")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("cipher.c2s")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("cipher.s2c")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("mac.c2s")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("mac.s2c")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("compression.c2s")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("compression.s2c")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("lang.c2s")));
			buf.putString(System.Text.Encoding.Default.GetBytes(conf.getSSHAlgorithm("lang.s2c")));

			buf.putByte((byte)0);
			buf.putInt(0);

			buf.setOffSet(5);
			I_C=new byte[buf.getLength()];
			buf.getByte(I_C);

			write(packet);
		}

		private void send_newkeys()  
		{
			// send SSH_MSG_NEWKEYS(21)
			packet.reset();
			buf.putByte((byte)SSH_MSG_NEWKEYS);
			write(packet);
		}

		private void checkHost(string host, KeyExchange kex)  
		{
			string shkc= conf.SSHStrictKeyCheck;

			byte[] K_S=kex.getHostKey();
			string key_type=kex.getKeyType();
			string key_fprint=kex.getFingerPrint();
			
			HostKeyRepository hkr= new KnownHosts(conf);
			int i=0;
			lock(hkr)
			{
				i=hkr.check(host, K_S);
			}

			bool insert=false;

			if((shkc == "ask" || shkc == "yes") && i==HostKeyRepository.CHANGED)
			{
				string file=null;
				lock(hkr)
				{
					file=hkr.getKnownHostsRepositoryID();
				}
				if(file==null){file="known_hosts";}
				string message=
					"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!\n"+
					"IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"+
					"Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n"+
					"It is also possible that the "+key_type+" host key has just been changed.\n"+
					"The fingerprint for the "+key_type+" key sent by the remote host is\n"+
					key_fprint+".\n"+
					"Please contact your system administrator.\n"+
					"Add correct host key in "+file+" to get rid of this message.";

				bool b=false;

				if(dhost!=null)
				{
					b=dhost.DisplayDialogQuestion(message+
						"\nDo you want to delete the old key and insert the new key?");
				}
				
				if(!b)
				{
					throw new Exception("HostKey has been changed: "+host);
				}
				else
				{
					lock(hkr)
					{
						string foo = "";
						if(key_type == "DSA")
							foo = "ssh-dss";
						else
							foo = "ssh-rsa";
						
						hkr.remove(host, foo, null);
						insert=true;
					}
				}
			}

			//insert=true;

			if((shkc == "ask" || shkc == "yes") && (i!=HostKeyRepository.OK) && !insert)
			{
				if(shkc == "yes")
				{
					throw new Exception("reject HostKey: "+host);
				}
				
				if(dhost!=null)
				{
					bool foo=dhost.DisplayDialogQuestion(
						"The authenticity of host '"+host+"' can't be established.\n"+
						key_type+" key fingerprint is "+key_fprint+".\n"+
						"Are you sure you want to continue connecting?"
						);
					if(!foo)
					{
						throw new Exception("reject HostKey: "+host);
					}
					insert=true;
				}
				else
				{
					if(i==HostKeyRepository.NOT_INCLUDED)
						throw new Exception("UnknownHostKey: "+host+". "+key_type+" key fingerprint is "+key_fprint);
					else throw new Exception("HostKey has been changed: "+host);
				}
			}

			if(shkc == "no" && HostKeyRepository.NOT_INCLUDED==i)
			{
				insert=true;
		    }

			if(insert)
			{
				lock(hkr)
				{

					HostKey myhostkey = new HostKey(conf, host, 0,  K_S);
					myhostkey.hash();					
					hkr.add(myhostkey, DialogHost);

				}
			}
		}

		public Channel openChannel(string type) 
		{
			if(!_isConnected)
			{
				throw new Exception("session is down");
			}
			try
			{
				Channel channel=Channel.getChannel(type);
				addChannel(channel);
				channel.init();
				return channel;
			}
			catch(Exception e)
			{
				System.Console.WriteLine(e);
			}
			return null;
		}

		// encode will bin invoked in write with synchronization.
		public void encode(Packet packet) 
		{
			if(deflater!=null)
			{
				packet.buffer.index=deflater.compress(packet.buffer.buffer,
					5, packet.buffer.index);
			}
			if(c2scipher!=null)
			{
				packet.padding(c2scipher.IVSize);
				int pad=packet.buffer.buffer[4];
				lock(random)
				{
					random.fill(packet.buffer.buffer, packet.buffer.index-pad, pad);
				}
			}
			else
			{
				packet.padding(8);
			}
			byte[] mac=null;
			if(c2smac!=null)
			{
				c2smac.update(seqo);
				c2smac.update(packet.buffer.buffer, 0, packet.buffer.index);
				mac=c2smac.doFinal();
			}
			if(c2scipher!=null)
			{
				byte[] buf=packet.buffer.buffer;
				c2scipher.update(buf, 0, packet.buffer.index, buf, 0);
			}
			if(mac!=null)
			{
				packet.buffer.putByte(mac);
			}
		}

		int[] uncompress_len=new int[1];

		private int cipher_size=8;
		public Buffer read(Buffer buf) 
		{
			int j=0;
			while(true)
			{
				buf.reset();
				io.getByte(buf.buffer, buf.index, cipher_size); buf.index+=cipher_size;
				if(s2ccipher!=null)
				{
					s2ccipher.update(buf.buffer, 0, cipher_size, buf.buffer, 0);
				}

				j=Util.ToInt32( buf.buffer, 0 );
				j=j-4-cipher_size+8;
				if(j<0 || (buf.index+j)>buf.buffer.Length)
				{
					throw new IOException("invalid data");
				}
				if(j>0)
				{
					io.getByte(buf.buffer, buf.index, j); buf.index+=(j);
					if(s2ccipher!=null)
					{
						s2ccipher.update(buf.buffer, cipher_size, j, buf.buffer, cipher_size);
					}
				}

				if(s2cmac!=null)
				{
					s2cmac.update(seqi);
					s2cmac.update(buf.buffer, 0, buf.index);
					byte[] result=s2cmac.doFinal();
					io.getByte(mac_buf, 0, mac_buf.Length);
					if(!Util.equals(result, mac_buf))
					{
						throw new IOException("MAC Error");
					}
				}
				seqi++;

				if(inflater!=null)
				{
					//inflater.uncompress(buf);
					int pad=buf.buffer[4];
					uncompress_len[0]=buf.index-5-pad;
					byte[] foo=inflater.uncompress(buf.buffer, 5, uncompress_len);
					if(foo!=null)
					{
						buf.buffer=foo;
						buf.index=5+uncompress_len[0];
					}
					else
					{
						System.Console.Error.WriteLine("fail in inflater");
						break;
					}
				}

				int type=buf.buffer[5]&0xff;
				if(type==SSH_MSG_DISCONNECT)
				{
					buf.rewind();
					buf.getInt();buf.getShort();
					int reason_code=buf.getInt();
					byte[] description=buf.getString();
					byte[] language_tag=buf.getString();
					
					throw new Exception("SSH_MSG_DISCONNECT:"+
						" "+reason_code+
						" "+ System.Text.Encoding.UTF8.GetString(description)+
						" "+ System.Text.Encoding.UTF8.GetString(language_tag));
					
				}
				else if(type==SSH_MSG_IGNORE)
				{
				}
				else if(type==SSH_MSG_DEBUG)
				{
					buf.rewind();
					buf.getInt();buf.getShort();
				}
				else if(type==SSH_MSG_CHANNEL_WINDOW_ADJUST)
				{
					buf.rewind();
					buf.getInt();buf.getShort();
					Channel c=Channel.getChannel(buf.getInt(), this);
					if(c==null)
					{
					}
					else
					{
						c.addRemoteWindowSize(buf.getInt());
					}
				}
				else
				{
					break;
				}
			}
			buf.rewind();
			return buf;
		}

		

		private void receive_newkeys(Buffer buf, KeyExchange kex)  
		{
			updateKeys(kex);
			in_kex=false;
		}
		private void updateKeys(KeyExchange kex) 
		{
			byte[] K=kex.getK();
			byte[] H=kex.getH();
			HASH hash=kex.getHash();

			string[] guess=kex._guess;

			if(session_id==null)
			{
				session_id=new byte[H.Length];
				Util.arraycopy(H, 0, session_id, 0, H.Length);
			}

			/*
			  Initial IV client to server:     HASH (K || H || "A" || session_id)
			  Initial IV server to client:     HASH (K || H || "B" || session_id)
			  Encryption key client to server: HASH (K || H || "C" || session_id)
			  Encryption key server to client: HASH (K || H || "D" || session_id)
			  Integrity key client to server:  HASH (K || H || "E" || session_id)
			  Integrity key server to client:  HASH (K || H || "F" || session_id)
			*/

			buf.reset();
			buf.putMPInt(K);
			buf.putByte(H);
			buf.putByte((byte)0x41);
			buf.putByte(session_id);
			hash.update(buf.buffer, 0, buf.index);
			IVc2s=hash.digest();

			int j=buf.index-session_id.Length-1;

			buf.buffer[j]++;
			hash.update(buf.buffer, 0, buf.index);
			IVs2c=hash.digest();

			buf.buffer[j]++;
			hash.update(buf.buffer, 0, buf.index);
			Ec2s=hash.digest();

			buf.buffer[j]++;
			hash.update(buf.buffer, 0, buf.index);
			Es2c=hash.digest();

			buf.buffer[j]++;
			hash.update(buf.buffer, 0, buf.index);
			MACc2s=hash.digest();

			buf.buffer[j]++;
			hash.update(buf.buffer, 0, buf.index);
			MACs2c=hash.digest();

			try
			{
				s2ccipher=(Cipher)System.Activator.CreateInstance(System.Type.GetType(conf.getSSHAlgorithm(guess[KeyExchange.PROPOSAL_ENC_ALGS_STOC])));
				while(s2ccipher.BlockSize >Es2c.Length)
				{
					buf.reset();
					buf.putMPInt(K);
					buf.putByte(H);
					buf.putByte(Es2c);
					hash.update(buf.buffer, 0, buf.index);
					byte[] foo=hash.digest();
					byte[] bar=new byte[Es2c.Length+foo.Length];
					Util.arraycopy(Es2c, 0, bar, 0, Es2c.Length);
					Util.arraycopy(foo, 0, bar, Es2c.Length, foo.Length);
					Es2c=bar;
				}
				s2ccipher.init(Cipher.DECRYPT_MODE, Es2c, IVs2c);
				cipher_size = s2ccipher.IVSize;
				s2cmac=(MAC)System.Activator.CreateInstance(System.Type.GetType(conf.getSSHAlgorithm(guess[KeyExchange.PROPOSAL_MAC_ALGS_STOC])));
				s2cmac.init(MACs2c);
				mac_buf=new byte[s2cmac.BlockSize];

				c2scipher=(Cipher)System.Activator.CreateInstance(System.Type.GetType(conf.getSSHAlgorithm(guess[KeyExchange.PROPOSAL_ENC_ALGS_CTOS])));
				while(c2scipher.BlockSize > Ec2s.Length)
				{
					buf.reset();
					buf.putMPInt(K);
					buf.putByte(H);
					buf.putByte(Ec2s);
					hash.update(buf.buffer, 0, buf.index);
					byte[] foo=hash.digest();
					byte[] bar=new byte[Ec2s.Length+foo.Length];
					Util.arraycopy(Ec2s, 0, bar, 0, Ec2s.Length);
					Util.arraycopy(foo, 0, bar, Ec2s.Length, foo.Length);
					Ec2s=bar;
				}
				c2scipher.init(Cipher.ENCRYPT_MODE, Ec2s, IVc2s);

				c2smac=(MAC)System.Activator.CreateInstance(System.Type.GetType(conf.getSSHAlgorithm(guess[KeyExchange.PROPOSAL_MAC_ALGS_CTOS])));
				c2smac.init(MACc2s);

				if(guess[KeyExchange.PROPOSAL_COMP_ALGS_CTOS] != "none")
				{
					string foo = conf.getSSHAlgorithm(guess[KeyExchange.PROPOSAL_COMP_ALGS_CTOS]);
					if(foo!=null)
					{
						try
						{
							deflater=(Compression)System.Activator.CreateInstance(System.Type.GetType(foo));
							int level=6;
							deflater.init(Compression.DEFLATER, level);
						}
						catch(Exception)
						{
							System.Console.Error.WriteLine(foo+" isn't accessible.");
						}
					}
				}
				else
				{
					if(deflater!=null)
					{
						deflater=null;
					}
				}
				if(guess[KeyExchange.PROPOSAL_COMP_ALGS_STOC] != "none")
				{
					string foo = conf.getSSHAlgorithm(guess[KeyExchange.PROPOSAL_COMP_ALGS_STOC]);
					if(foo!=null)
					{
						try
						{
							inflater=(Compression)System.Activator.CreateInstance(System.Type.GetType(foo));
							inflater.init(Compression.INFLATER, 0);
						}
						catch(Exception)
						{
							System.Console.Error.WriteLine(foo+" isn't accessible.");
						}
					}
				}
				else
				{
					if(inflater!=null)
					{
						inflater=null;
					}
				}
			}
			catch(Exception e){ System.Console.Error.WriteLine("updatekeys: "+e); }
		}

		public void write(Packet packet, Channel c, int length) 
		{
			while(true)
			{
				if(in_kex)
				{
					try{Thread.Sleep(10);}
					catch(ThreadInterruptedException){};
					continue;
				}
				lock(c)
				{
					if(c.rwsize>=length)
					{
						c.rwsize-=length;
						break;
					}
				}
				if(c._close || !c.isConnected())
				{
					throw new IOException("channel is broken");
				}

				bool sendit=false;
				int s=0;
				byte command=0;
				int recipient=-1;
				lock(c)
				{
					if(c.rwsize>0)
					{
						int len=c.rwsize;
						if(len>length)
						{
							len=length;
						}
						if(len!=length)
						{
							s=packet.shift(len, (c2smac!=null ? c2smac.BlockSize : 0));
						}
						command=packet.buffer.buffer[5];
						recipient=c.getRecipient();
						length-=len;
						c.rwsize-=len;
						sendit=true;
					}
				}
				if(sendit)
				{
					_write(packet);
					if(length==0)
					{
						return;
					}
					packet.unshift(command, recipient, s, length);
					lock(c)
					{
						if(c.rwsize>=length)
						{
							c.rwsize-=length;
							break;
						}
					}
				}

				try{Thread.Sleep(100);}
				catch(ThreadInterruptedException){};
			}
			_write(packet);
		}
		
		public void write(Packet packet) 
		{
			
			while(in_kex)
			{
				byte command=packet.buffer.buffer[5];
				
				if(command==SSH_MSG_KEXINIT ||
					command==SSH_MSG_NEWKEYS ||
					command==SSH_MSG_KEXDH_INIT ||
					command==SSH_MSG_KEXDH_REPLY ||
					command==SSH_MSG_DISCONNECT ||
					command==SSH_MSG_KEX_DH_GEX_GROUP ||
					command==SSH_MSG_KEX_DH_GEX_INIT ||
					command==SSH_MSG_KEX_DH_GEX_REPLY ||
					command==SSH_MSG_KEX_DH_GEX_REQUEST)
				{				
					break;
				}
				try{Thread.Sleep(10);}
				catch(ThreadInterruptedException){};
			}
			_write(packet);
		}
		[System.Runtime.CompilerServices.MethodImpl(MethodImplOptions.Synchronized)]
		private void _write(Packet packet) 
		{
			encode(packet);
			if(io!=null)
			{
				io.put(packet);
				seqo++;
			}
		}

		
		public void run()
		{
			byte[] foo;
			Buffer buf=new Buffer();
			Packet packet=new Packet(buf);
			int i=0;
			Channel channel;
			int[] start=new int[1];
			int[] length=new int[1];
			KeyExchange kex=null;

			try
			{
				while(_isConnected)
				{
					buf=read(buf);
					int msgType=buf.buffer[5]&0xff;
		
					if(kex!=null && kex.getState()==msgType)
					{
						bool result=kex.next(buf);
						if(!result)
						{
							throw new Exception("verify: "+result);
						}
						continue;
					}

					switch(msgType)
					{
						case SSH_MSG_KEXINIT:
							//System.Console.WriteLine("KEXINIT");
							kex=receive_kexinit(buf);
							break;

						case SSH_MSG_NEWKEYS:
							//System.Console.WriteLine("NEWKEYS");
							send_newkeys();
							receive_newkeys(buf, kex);
							kex=null;
							break;

					case SSH_MSG_CHANNEL_DATA:
						//System.Console.WriteLine("DATA");						
						buf.getInt();
						buf.getByte();
						buf.getByte();
						i=buf.getInt();
						channel=Channel.getChannel(i, this);
						foo=buf.getString(start, length);
							if(channel==null)
							{
							break;
					}
							try
							{
								channel.write(foo, start[0], length[0]);
							}
							catch(Exception)
							{
								//System.Console.WriteLine(e);
								try{channel.disconnect();}
								catch(Exception){}
								break;
							}
							int len=length[0];
							channel.setLocalWindowSize(channel.lwsize-len);
							if(channel.lwsize<channel.lwsize_max/2)
							{
								packet.reset();
								buf.putByte((byte)SSH_MSG_CHANNEL_WINDOW_ADJUST);
								buf.putInt(channel.getRecipient());
								buf.putInt(channel.lwsize_max-channel.lwsize);
								write(packet);
								channel.setLocalWindowSize(channel.lwsize_max);
							}
							break;

					case SSH_MSG_CHANNEL_WINDOW_ADJUST:
						//System.Console.WriteLine("WINDOW_ADJUST");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							if(channel==null)
							{
								break;
							}
							channel.addRemoteWindowSize(buf.getInt());
							break;

					case SSH_MSG_CHANNEL_EOF:
						//System.Console.WriteLine("EOF");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							if(channel!=null)
							{
								channel.eof_remote();
							}
							break;
					case SSH_MSG_CHANNEL_CLOSE:
						//System.Console.WriteLine("CLOSE");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							if(channel!=null)
							{
								channel.disconnect();
							}
							break;
					case SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
						//System.Console.WriteLine("OPEN_CONFIRMATION");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							channel.setRecipient(buf.getInt());
							channel.setRemoteWindowSize(buf.getInt());
							channel.setRemotePacketSize(buf.getInt());
							break;
					case SSH_MSG_CHANNEL_OPEN_FAILURE:
						//System.Console.WriteLine("OPEN_FAILURE");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							int reason_code=buf.getInt();
							channel.exitstatus=reason_code;
							channel._close=true;
							channel._eof_remote=true;
							channel.setRecipient(0);
							break;
					case SSH_MSG_CHANNEL_REQUEST:
						//System.Console.WriteLine("REQUEST");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							foo=buf.getString();
							bool reply=(buf.getByte()!=0);
							channel=Channel.getChannel(i, this);
							if(channel!=null)
							{
								byte reply_type=(byte)SSH_MSG_CHANNEL_FAILURE;
								if(System.Text.Encoding.UTF8.GetString(foo) == "exit-status")
								{
									i=buf.getInt();             // exit-status
									channel.setExitStatus(i);
									reply_type=(byte)SSH_MSG_CHANNEL_SUCCESS;
								}
								if(reply)
								{
									packet.reset();
									buf.putByte(reply_type);
									buf.putInt(channel.getRecipient());
									write(packet);
								}
							}
							else
							{
							}
							break;
					case SSH_MSG_CHANNEL_OPEN:
						//System.Console.WriteLine("OPEN");
							buf.getInt();
							buf.getShort();
							foo=buf.getString();
							string ctyp= System.Text.Encoding.UTF8.GetString(foo);
							channel=Channel.getChannel(ctyp);
							addChannel(channel);
							channel.getData(buf);
							channel.init();
							packet.reset();
							buf.putByte((byte)SSH_MSG_CHANNEL_OPEN_CONFIRMATION);
							buf.putInt(channel.getRecipient());
							buf.putInt(channel.id);
							buf.putInt(channel.lwsize);
							buf.putInt(channel.lmpsize);
							write(packet);
							Thread tmp=new Thread(channel.run);
							tmp.Name = "Channel "+ctyp+" "+host;
							tmp.Start();
							break;
					case SSH_MSG_CHANNEL_SUCCESS:
						//System.Console.WriteLine("SUCCESS");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							if(channel==null)
							{
								break;
							}
							channel.reply=1;
							break;
					case SSH_MSG_CHANNEL_FAILURE:
						//System.Console.WriteLine("FAILURE");
							buf.getInt();
							buf.getShort();
							i=buf.getInt();
							channel=Channel.getChannel(i, this);
							if(channel==null)
							{
								break;
							}
							channel.reply=0;
							break;
					case SSH_MSG_GLOBAL_REQUEST:
						//System.Console.WriteLine("GLOBAL_REQUEST");
							buf.getInt();
							buf.getShort();
							foo=buf.getString();       // request name
							reply=(buf.getByte()!=0);
							if(reply)
							{
								packet.reset();
								buf.putByte((byte)SSH_MSG_REQUEST_FAILURE);
								write(packet);
							}
							break;
						case SSH_MSG_REQUEST_FAILURE:
					case SSH_MSG_REQUEST_SUCCESS:
						//System.Console.WriteLine("REQUEST_SUCCESS");
							Thread t=grr.getThread();
							if(t!=null)
							{
								grr.setReply(msgType==SSH_MSG_REQUEST_SUCCESS? 1 : 0);
								t.Interrupt();
							}
							break;
						default:
							System.Console.WriteLine("Session.run: unsupported type "+msgType);
							throw new IOException("Unknown SSH message type "+msgType);
					}
				}
			}
			catch(Exception)
			{
				
			}
			try
			{
				disconnect();
			}
			catch(NullReferenceException)
			{
				
			}
			catch(Exception)
			{
				
			}
			_isConnected=false;
		}


		public void disconnect()
		{
			if(!_isConnected) return;


			Channel.disconnect(this);

			_isConnected=false;

			
			lock(connectThread)
			{
				connectThread.Interrupt();
				connectThread=null;
			}
			//thread=null;
			try
			{
				if(io!=null)
				{
					if(io.ins!=null) io.ins.Close();
					if(io.outs!=null) io.outs.Close();
			
				}
				
				if(socket!=null)
					socket.Close();
				
				
			}
			catch(Exception)
			{
			
			}
			io=null;
			socket=null;
			
			//jsch.removeSession(this);

		}
		
		private class GlobalRequestReply
		{
			private Thread thread=null;
			private int reply=-1;
			internal void setThread(Thread thread)
			{
				this.thread=thread;
				this.reply=-1;
			}
			internal Thread getThread(){ return thread; }
			internal void setReply(int reply){ this.reply=reply; }
			internal int getReply(){ return this.reply; }
		}
		private GlobalRequestReply grr=new GlobalRequestReply();
		
		internal void addChannel(Channel channel)
		{
			channel.session=this;
	    }

		public void setHost(string host){ this.host=host; }
		public void setPort(int port){ this.port=port; }
		public void setPassword(string foo){ this.password=foo; }
		public void setUserInfo(UserInfo userinfo){ this.userinfo=userinfo; }
		public void setInputStream(Stream In){ this.In=In; }
		public void setOutputStream(Stream Out){ this.Out=Out; }
		
		public bool isConnected(){ return _isConnected; }
		public int getTimeout(){ return timeout; }
		public void setTimeout(int foo)  
		{
			if(socket==null)
			{
				if(foo<0)
				{
					throw new Exception("invalid timeout value");
				}
				this.timeout=foo;
				return;
			}
			try
			{
				socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, foo);
				socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, foo);
				timeout=foo;
			}
			catch(Exception e)
			{
				throw new Exception(e.ToString());
			}
		}
		public string getServerVersion()
		{
			return System.Text.Encoding.UTF8.GetString(V_S);
		}
		
		public void sendIgnore() 
		{
			Buffer buf=new Buffer();
			Packet packet=new Packet(buf);
			packet.reset();
			buf.putByte((byte)SSH_MSG_IGNORE);
			write(packet);
		}
		
		private static byte[] keepalivemsg = System.Text.Encoding.UTF8.GetBytes("keepalive@jcraft.com");
		public void sendKeepAliveMsg()
		{
			Buffer buf=new Buffer();
			Packet packet=new Packet(buf);
			packet.reset();
			buf.putByte((byte)SSH_MSG_GLOBAL_REQUEST);
			buf.putString(keepalivemsg);
			buf.putByte((byte)1);
			write(packet);
		}

		private HostKey hostkey=null;
		public HostKey getHostKey(){ return hostkey; }
		public string getHost(){return host;}
		public string getUserName(){return username;}
		public int getPort(){return port;}

		public string getMac()
		{
			string mac="";
			if(s2cmac!=null)
				mac = s2cmac.Name;
			return mac;

		}
		public string getCipher()
		{
			string cipher="";
			if(s2ccipher!=null)
				cipher= s2ccipher.ToString();
			return cipher;
		}

		public int BufferLength
		{
			get{return this.buf.buffer.Length;}
		}
		
        public event EventHandler LogTextEmitted;
		public virtual void OnLogTextEmitted(LogTextEmittedArgs e)
		{
			LogTextEmitted(this, e);
		}	
	}

}
