/*
* Copyright (c) 2013-2026, The PurpleI2P Project
*
* This file is part of Purple i2pd project and licensed under BSD3
*
* See full license text in LICENSE file at top of project tree
*/

#include <string.h>
#include "Log.h"
#include "ClientContext.h"
#include "util.h"
#include "BOB.h"

namespace i2p
{
namespace client
{
	void BOBI2PTunnelIncomingConnection::Established ()
	{
		if (m_IsQuiet)
			StreamReceive ();
		else
		{
			// send destination first like received from I2P
			std::string dest = GetStream ()->GetRemoteIdentity ()->ToBase64 ();
			dest += "\n";
			if (dest.size() <= I2P_TUNNEL_CONNECTION_BUFFER_SIZE)
				memcpy (GetStreamBuffer (), dest.c_str (), dest.size ());
			else
				memset (GetStreamBuffer (), 0, I2P_TUNNEL_CONNECTION_BUFFER_SIZE);
			HandleStreamReceive (boost::system::error_code (), dest.size ());
		}
		Receive ();
	}

	BOBI2PInboundTunnel::BOBI2PInboundTunnel (const boost::asio::ip::tcp::endpoint& ep, std::shared_ptr<ClientDestination> localDestination):
		BOBI2PTunnel (localDestination), m_Acceptor (localDestination->GetService (), ep)
	{
	}

	BOBI2PInboundTunnel::~BOBI2PInboundTunnel ()
	{
		Stop ();
	}

	void BOBI2PInboundTunnel::Start ()
	{
		m_Acceptor.listen ();
		Accept ();
	}

	void BOBI2PInboundTunnel::Stop ()
	{
		m_Acceptor.close();
		ClearHandlers ();
	}

	void BOBI2PInboundTunnel::Accept ()
	{
		auto receiver = std::make_shared<AddressReceiver> ();
		receiver->socket = std::make_shared<boost::asio::ip::tcp::socket> (GetService ());
		m_Acceptor.async_accept (*receiver->socket, std::bind (&BOBI2PInboundTunnel::HandleAccept, this,
			std::placeholders::_1, receiver));
	}

	void BOBI2PInboundTunnel::HandleAccept (const boost::system::error_code& ecode, std::shared_ptr<AddressReceiver> receiver)
	{
		if (!ecode)
		{
			Accept ();
			ReceiveAddress (receiver);
		}
	}

	void BOBI2PInboundTunnel::ReceiveAddress (std::shared_ptr<AddressReceiver> receiver)
	{
		receiver->socket->async_read_some (boost::asio::buffer(
			receiver->buffer + receiver->bufferOffset,
			BOB_COMMAND_BUFFER_SIZE - receiver->bufferOffset),
			std::bind(&BOBI2PInboundTunnel::HandleReceivedAddress, this,
			std::placeholders::_1, std::placeholders::_2, receiver));
	}

	void BOBI2PInboundTunnel::HandleReceivedAddress (const boost::system::error_code& ecode, std::size_t bytes_transferred,
		std::shared_ptr<AddressReceiver> receiver)
	{
		if (ecode)
			LogPrint (eLogError, "BOB: Inbound tunnel read error: ", ecode.message ());
		else
		{
			receiver->bufferOffset += bytes_transferred;
			receiver->buffer[receiver->bufferOffset] = 0;
			char * eol = strchr (receiver->buffer, '\n');
			if (eol)
			{
				*eol = 0;
				if (eol != receiver->buffer && eol[-1] == '\r') eol[-1] = 0; // workaround for Transmission, it sends '\r\n' terminated address
				receiver->data = (uint8_t *)eol + 1;
				receiver->dataLen = receiver->bufferOffset - (eol - receiver->buffer + 1);
				auto addr = context.GetAddressBook ().GetAddress (receiver->buffer);
				if (!addr)
				{
					LogPrint (eLogError, "BOB: Address ", receiver->buffer, " not found");
					return;
				}
				if (addr->IsIdentHash ())
				{
					auto leaseSet = GetLocalDestination ()->FindLeaseSet (addr->identHash);
					if (leaseSet)
						CreateConnection (receiver, leaseSet);
					else
						GetLocalDestination ()->RequestDestination (addr->identHash,
							std::bind (&BOBI2PInboundTunnel::HandleDestinationRequestComplete,
							this, std::placeholders::_1, receiver));
				}
				else
					GetLocalDestination ()->RequestDestinationWithEncryptedLeaseSet (addr->blindedPublicKey,
							std::bind (&BOBI2PInboundTunnel::HandleDestinationRequestComplete,
							this, std::placeholders::_1, receiver));
			}
			else
			{
				if (receiver->bufferOffset < BOB_COMMAND_BUFFER_SIZE)
					ReceiveAddress (receiver);
				else
					LogPrint (eLogError, "BOB: Missing inbound address");
			}
		}
	}

	void BOBI2PInboundTunnel::HandleDestinationRequestComplete (std::shared_ptr<i2p::data::LeaseSet> leaseSet, std::shared_ptr<AddressReceiver> receiver)
	{
		if (leaseSet)
			CreateConnection (receiver, leaseSet);
		else
			LogPrint (eLogError, "BOB: LeaseSet for inbound destination not found");
	}

	void BOBI2PInboundTunnel::CreateConnection (std::shared_ptr<AddressReceiver> receiver, std::shared_ptr<const i2p::data::LeaseSet> leaseSet)
	{
		LogPrint (eLogDebug, "BOB: New inbound connection");
		auto connection = std::make_shared<I2PTunnelConnection>(shared_from_this (), receiver->socket, leaseSet);
		AddHandler (connection);
		connection->I2PConnect (receiver->data, receiver->dataLen);
	}

	BOBI2POutboundTunnel::BOBI2POutboundTunnel (const std::string& outhost, uint16_t port,
		std::shared_ptr<ClientDestination> localDestination, bool quiet): BOBI2PTunnel (localDestination),
		m_Endpoint (boost::asio::ip::make_address (outhost), port), m_IsQuiet (quiet)
	{
	}

	void BOBI2POutboundTunnel::Start ()
	{
		Accept ();
	}

	void BOBI2POutboundTunnel::Stop ()
	{
		auto localDestination = GetLocalDestination ();
		if (localDestination) localDestination->StopAcceptingStreams ();
		ClearHandlers ();
	}

	void BOBI2POutboundTunnel::Accept ()
	{
		auto localDestination = GetLocalDestination ();
		if (localDestination)
			localDestination->AcceptStreams (std::bind (&BOBI2POutboundTunnel::HandleAccept, this, std::placeholders::_1));
		else
			LogPrint (eLogError, "BOB: Local destination not set for server tunnel");
	}

	void BOBI2POutboundTunnel::HandleAccept (std::shared_ptr<i2p::stream::Stream> stream)
	{
		if (stream)
		{
			auto conn = std::make_shared<BOBI2PTunnelIncomingConnection> (shared_from_this (), stream, m_Endpoint, m_IsQuiet);
			AddHandler (conn);
			conn->Connect ();
		}
	}

	BOBDestination::BOBDestination (std::shared_ptr<ClientDestination> localDestination,
			const std::string &nickname, const std::string &inhost, const std::string &outhost,
			const uint16_t inport, const uint16_t outport, const bool quiet):
		m_LocalDestination (localDestination),
		m_OutboundTunnel (nullptr), m_InboundTunnel (nullptr),
		m_Nickname(nickname), m_InHost(inhost), m_OutHost(outhost),
		m_InPort(inport), m_OutPort(outport), m_Quiet(quiet), m_IsRunning(false)
	{
	}

	BOBDestination::~BOBDestination ()
	{
		i2p::client::context.DeleteLocalDestination (m_LocalDestination);
	}

	void BOBDestination::Start ()
	{
		if (m_OutboundTunnel) m_OutboundTunnel->Start ();
		if (m_InboundTunnel) m_InboundTunnel->Start ();
		m_IsRunning = true;
	}

	void BOBDestination::Stop ()
	{
		StopTunnels ();
		m_LocalDestination->Stop ();
	}

	void BOBDestination::StopTunnels ()
	{
		m_IsRunning = false;
		if (m_OutboundTunnel)
		{
			m_OutboundTunnel->Stop ();
			m_OutboundTunnel = nullptr;
		}
		if (m_InboundTunnel)
		{
			m_InboundTunnel->Stop ();
			m_InboundTunnel = nullptr;
		}
	}

	void BOBDestination::CreateInboundTunnel (uint16_t port, const std::string& inhost)
	{
		if (!m_InboundTunnel)
		{
			// update inport and inhost (user can stop tunnel and change)
			m_InPort = port;
			m_InHost = inhost;
			boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port);
			if (!inhost.empty ())
			{
				boost::system::error_code ec;
				auto addr = boost::asio::ip::make_address (inhost, ec);
				if (!ec)
					ep.address (addr);
				else
					LogPrint (eLogError, "BOB: ", ec.message ());
			}
			m_InboundTunnel = std::make_shared<BOBI2PInboundTunnel> (ep, m_LocalDestination);
		}
	}

	void BOBDestination::CreateOutboundTunnel (const std::string& outhost, uint16_t port, bool quiet)
	{
		if (!m_OutboundTunnel)
		{
			// update outport and outhost (user can stop tunnel and change)
			m_OutPort = port;
			m_OutHost = outhost;
			m_OutboundTunnel = std::make_shared<BOBI2POutboundTunnel> (outhost, port, m_LocalDestination, quiet);
		}
	}

	BOBCommandSession::BOBCommandSession (BOBCommandChannel& owner):
		m_Owner (owner), m_Socket (m_Owner.GetService ()),
		m_ReceiveBuffer(BOB_COMMAND_BUFFER_SIZE + 1), m_SendBuffer(BOB_COMMAND_BUFFER_SIZE + 1),
		m_IsOpen (true), m_IsQuiet (false), m_IsActive (false),
		m_InPort (0), m_OutPort (0), m_CurrentDestination (nullptr)
	{
	}

	BOBCommandSession::~BOBCommandSession ()
	{
	}

	void BOBCommandSession::Terminate ()
	{
		#ifdef MAC_OSX
			m_Socket.cancel ();
		#endif
		m_Socket.close ();
		m_IsOpen = false;
	}

	void BOBCommandSession::Receive ()
	{
		boost::asio::async_read_until(m_Socket, m_ReceiveBuffer, '\n',
			std::bind(&BOBCommandSession::HandleReceivedLine, shared_from_this(),
				std::placeholders::_1, std::placeholders::_2));
	}

	void BOBCommandSession::HandleReceivedLine(const boost::system::error_code& ecode, std::size_t bytes_transferred)
	{
		if(ecode)
		{
			LogPrint (eLogError, "BOB: Command channel read error: ", ecode.message());
			if (ecode != boost::asio::error::operation_aborted)
				Terminate ();
		}
		else
		{
			std::string line;

			std::istream is(&m_ReceiveBuffer);
			std::getline(is, line);

			std::string command, operand;
			std::istringstream iss(line);
			iss >> command >> operand;

			// process command
			auto& handlers = m_Owner.GetCommandHandlers();
			auto it = handlers.find(command);
			if(it != handlers.end())
			{
				(this->*(it->second))(operand.c_str(), operand.length());
			}
			else
			{
				LogPrint (eLogError, "BOB: Unknown command ", command.c_str());
				SendReplyError ("unknown command");
			}
		}
	}

	void BOBCommandSession::Send ()
	{
		boost::asio::async_write (m_Socket, m_SendBuffer,
			boost::asio::transfer_all (),
			std::bind(&BOBCommandSession::HandleSent, shared_from_this (),
				std::placeholders::_1, std::placeholders::_2));
	}

	void BOBCommandSession::HandleSent (const boost::system::error_code& ecode, std::size_t bytes_transferred)
	{
		if (ecode)
		{
			LogPrint (eLogError, "BOB: Command channel send error: ", ecode.message ());
			if (ecode != boost::asio::error::operation_aborted)
				Terminate ();
		}
		else
		{
			if (m_IsOpen)
				Receive ();
			else
				Terminate ();
		}
	}

	void BOBCommandSession::SendReplyOK (std::string_view msg)
	{
		std::ostream os(&m_SendBuffer);
		os << "OK";
		if (!msg.empty ())
			os << " " << msg;
		os << std::endl;
		Send ();
	}

	void BOBCommandSession::SendReplyOK (const std::vector<std::string_view>& strings)
	{
		std::ostream os(&m_SendBuffer);
		os << "OK";
		if (!strings.empty ()) os << " ";
		for (auto& it: strings)
			os << it;
		os << std::endl;
		Send ();
	}

	void BOBCommandSession::SendReplyError (std::string_view msg)
	{
		std::ostream os(&m_SendBuffer);
		os << "ERROR " << msg << std::endl;
		Send ();
	}

	void BOBCommandSession::SendVersion ()
	{
		std::ostream os(&m_SendBuffer);
		os << "BOB 00.00.10" << std::endl;
		SendReplyOK(std::string_view()); // empty string
	}

	void BOBCommandSession::SendRaw (std::string_view data)
	{
		std::ostream os(&m_SendBuffer);
		os << data << std::endl;
	}

	void BOBCommandSession::BuildStatusLine(bool currentTunnel, std::shared_ptr<BOBDestination> dest, std::string &out)
	{
		// helper lambdas
		const auto issetStr = [](const std::string &str) { return str.empty() ? "not_set" : str; }; // for inhost, outhost
		const auto issetNum = [&issetStr](const int p) { return issetStr(p == 0 ? "" : std::to_string(p)); }; // for inport, outport
		const auto destExists = [](const BOBDestination * const dest) { return dest != nullptr; };
		const auto destReady = [](const BOBDestination * const dest) { return dest && dest->IsRunning(); };
		const auto bool_str = [](const bool v) { return v ? "true" : "false"; }; // bool -> str
		const auto getProxyType = [](const i2p::client::I2PService* proxy) -> std::string {
		if (!proxy) return "NONE";
		if (dynamic_cast<const i2p::proxy::SOCKSProxy*>(proxy)) return "SOCKS";
		if (dynamic_cast<const i2p::proxy::HTTPProxy*>(proxy)) return "HTTPPROXY";
		return "UNKNOWN";
		};
		const auto isProxyRunning = [](const i2p::client::I2PService* proxy) -> bool {
		return proxy != nullptr;
		};

		// tunnel info
		const std::string nickname = currentTunnel ? m_Nickname : dest->GetNickname();
		const bool quiet = currentTunnel ? m_IsQuiet : dest->GetQuiet();
		const std::string inhost = issetStr(currentTunnel ? m_InHost : dest->GetInHost());
		const std::string outhost = issetStr(currentTunnel ? m_OutHost : dest->GetOutHost());
		const std::string inport = issetNum(currentTunnel ? m_InPort : dest->GetInPort());
		const std::string outport = issetNum(currentTunnel ? m_OutPort : dest->GetOutPort());
		const bool keys = destExists(dest.get ()); // key must exist when destination is created
		const bool starting = destExists(dest.get ()) && !destReady(dest.get ());
		const bool running = destExists(dest.get ()) && destReady(dest.get ());
		const bool stopping = false;

		const i2p::client::I2PService* proxy = m_Owner.GetProxy(nickname);
		const std::string proxyType = getProxyType(proxy);
		const bool proxyStatus = isProxyRunning(proxy);

		// build line
		std::stringstream ss;
		ss	<< "DATA "
			<< "NICKNAME: " << nickname          << " " << "STARTING: " << bool_str(starting) << " "
			<< "RUNNING: "  << bool_str(running) << " " << "STOPPING: " << bool_str(stopping) << " "
			<< "KEYS: "     << bool_str(keys)    << " " << "QUIET: "    << bool_str(quiet) << " "
			<< "INPORT: "   << inport            << " " << "INHOST: "   << inhost << " "
			<< "OUTPORT: "  << outport           << " " << "OUTHOST: "  << outhost << " "
			<< "PROXYTYPE: "<< proxyType         << " " << "PROXYSTART: "  << bool_str(proxyStatus);
		out = ss.str();
	}

	void BOBCommandSession::ZapCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: zap");
		Terminate ();
	}

	void BOBCommandSession::QuitCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: quit");
		m_IsOpen = false;
		SendReplyOK ("Bye!");
	}

	void BOBCommandSession::StartCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: start ", m_Nickname);
		if (m_IsActive)
		{
			SendReplyError ("tunnel is active");
			return;
		}
		if (!m_Keys.GetPublic ()) // keys are set ?
		{
			SendReplyError("Keys must be set.");
			return;
		}
		if (m_InPort == 0
			&& m_OutHost.empty() && m_OutPort == 0)
		{
			SendReplyError("(inhost):inport or outhost:outport must be set.");
			return;
		}
		if(!m_InHost.empty())
		{
			// TODO: FIXME: temporary validation, until hostname support is added
			boost::system::error_code ec;
			boost::asio::ip::make_address(m_InHost, ec);
			if (ec)
			{
				SendReplyError("inhost must be a valid IPv4 address.");
				return;
			}
		}
		if(!m_OutHost.empty())
		{
			// TODO: FIXME: temporary validation, until hostname support is added
			boost::system::error_code ec;
			boost::asio::ip::make_address(m_OutHost, ec);
			if (ec)
			{
				SendReplyError("outhost must be a IPv4 address.");
				return;
			}
		}

		if (!m_CurrentDestination)
		{
			m_CurrentDestination = std::make_shared<BOBDestination> (i2p::client::context.CreateNewLocalDestination (m_Keys, true, &m_Options), // deleted in clear command
				m_Nickname, m_InHost, m_OutHost, m_InPort, m_OutPort, m_IsQuiet);
			m_Owner.AddDestination (m_Nickname, m_CurrentDestination);
		}
		if (!m_tunnelType.has_value())
		{
			if (m_InPort)
				m_CurrentDestination->CreateInboundTunnel (m_InPort, m_InHost);
			if (m_OutPort && !m_OutHost.empty ())
				m_CurrentDestination->CreateOutboundTunnel (m_OutHost, m_OutPort, m_IsQuiet);
			m_CurrentDestination->Start ();
		}
		else
		{
			switch (*m_tunnelType)
			{
				case TunnelType::SOCKS:
					try
					{
						auto SocksProxy = std::make_unique<i2p::proxy::SOCKSProxy>(m_Nickname, m_InHost, m_InPort,
							false, m_OutHost, m_OutPort, m_CurrentDestination->GetLocalDestination());
						SocksProxy->Start();
						m_Owner.SetProxy(m_Nickname, std::move(SocksProxy));
					}
					catch (std::exception& e)
					{
						LogPrint(eLogCritical, "Clients: Exception in SOCKS Proxy: ", e.what());
						ThrowFatal ("Unable to start SOCKS Proxy at ", m_InHost, ":", m_InPort, ": ", e.what ());
					}
					break;
				case TunnelType::HTTP_PROXY:
					try
					{
						auto HttpProxy = std::make_unique<i2p::proxy::HTTPProxy>(m_Nickname, m_InHost, m_InPort,
							m_OutHost, true, true, m_CurrentDestination->GetLocalDestination());
						HttpProxy->Start();
						m_Owner.SetProxy(m_Nickname, std::move(HttpProxy));
					}
					catch (std::exception& e)
					{
						LogPrint(eLogCritical, "Clients: Exception in HTTP Proxy: ", e.what());
						ThrowFatal ("Unable to start HTTP Proxy at ", m_InHost, ":", m_InPort, ": ", e.what ());
					}
					break;
				default:
					SendReplyError("Unsupported tunnel type.");
					return;
			}
		}
		SendReplyOK ("Tunnel starting");
		m_IsActive = true;
	}

	void BOBCommandSession::StopCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: stop ", m_Nickname);
		if (!m_IsActive)
		{
			SendReplyError ("tunnel is inactive");
			return;
		}
		auto dest = m_Owner.FindDestination (m_Nickname);
		auto proxy = m_Owner.GetProxy (m_Nickname);
		if (dest)
		{
			dest->StopTunnels ();
			SendReplyOK ("Tunnel stopping");
			if (proxy)
			{
				m_Owner.RemoveProxy (m_Nickname);
			}
		}
		else
			SendReplyError ("tunnel not found");
		m_IsActive = false;
	}

	void BOBCommandSession::SetNickCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: setnick ", operand);
		if(*operand)
		{
			auto dest = m_Owner.FindDestination (operand);
			if (!dest)
			{
				m_Nickname = operand;
				SendReplyOK ({ "Nickname set to ", m_Nickname });
			}
			else
				SendReplyError ("tunnel is active");
		}
		else
			SendReplyError ("no nickname has been set");
	}

	void BOBCommandSession::GetNickCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: getnick ", operand);
		if(*operand)
		{
			m_CurrentDestination = m_Owner.FindDestination (operand);
			auto proxy = m_Owner.GetProxy (operand);
			if (m_CurrentDestination)
			{
				m_Keys = m_CurrentDestination->GetKeys ();
				m_IsActive = m_CurrentDestination->IsRunning ();
				if(proxy)
					m_IsActive = true;
				m_Nickname = operand;
			}
			if (m_Nickname == operand)
				SendReplyOK ({"Nickname set to ", m_Nickname});
			else
				SendReplyError ("no nickname has been set");
		}
		else
			SendReplyError ("no nickname has been set");
	}

	void BOBCommandSession::NewkeysCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: newkeys");
		i2p::data::SigningKeyType signatureType = i2p::data::SIGNING_KEY_TYPE_EDDSA_SHA512_ED25519;
		i2p::data::CryptoKeyType cryptoType = i2p::data::CRYPTO_KEY_TYPE_ELGAMAL;
		if (*operand)
		{
			try
			{
				char * operand1 = (char *)strchr (operand, ' ');
				if (operand1)
				{
					*operand1 = 0; operand1++;
					cryptoType = std::stoi(operand1);
				}
				signatureType = std::stoi(operand);
			}
			catch (std::invalid_argument& ex)
			{
				LogPrint (eLogWarning, "BOB: Error on newkeys: ", ex.what ());
			}
		}


		m_Keys = i2p::data::PrivateKeys::CreateRandomKeys (signatureType, cryptoType, true);
		SendReplyOK (m_Keys.GetPublic ()->ToBase64 ());
	}

	void BOBCommandSession::SetkeysCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: setkeys ", operand);
		if (*operand && m_Keys.FromBase64 (operand))
			SendReplyOK (m_Keys.GetPublic ()->ToBase64 ());
		else
			SendReplyError ("invalid keys");
	}

	void BOBCommandSession::GetkeysCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: getkeys");
		if (m_Keys.GetPublic ()) // keys are set ?
			SendReplyOK (m_Keys.ToBase64 ());
		else
			SendReplyError ("keys are not set");
	}

	void BOBCommandSession::GetdestCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: getdest");
		if (m_Keys.GetPublic ()) // keys are set ?
			SendReplyOK (m_Keys.GetPublic ()->ToBase64 ());
		else
			SendReplyError ("keys are not set");
	}

	void BOBCommandSession::OuthostCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: outhost ", operand);
		if (*operand)
		{
			m_OutHost = operand;
			SendReplyOK ("outhost set");
		}
		else
			SendReplyError ("empty outhost");
	}

	void BOBCommandSession::OutportCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: outport ", operand);
		if (*operand)
		{
			int port = std::stoi(operand);
			if (port >= 0 && port < 65536)
			{
				m_OutPort = port;
				SendReplyOK ("outbound port set");
			}
			else
				SendReplyError ("port out of range");
		}
		else
			SendReplyError ("empty outport");
	}

	void BOBCommandSession::InhostCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: inhost ", operand);
		if (*operand)
		{
			m_InHost = operand;
			SendReplyOK ("inhost set");
		}
		else
			SendReplyError ("empty inhost");
	}

	void BOBCommandSession::InportCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: inport ", operand);
		if (*operand)
		{
			int port = std::stoi(operand);
			if (port >= 0 && port < 65536)
			{
				m_InPort = port;
				SendReplyOK ("inbound port set");
			}
			else
				SendReplyError ("port out of range");
		}
		else
			SendReplyError ("empty inport");
	}

	void BOBCommandSession::QuietCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: quiet");
		if (m_Nickname.length () > 0)
		{
			if (!m_IsActive)
			{
				m_IsQuiet = true;
				SendReplyOK ("Quiet set");
			}
			else
				SendReplyError ("tunnel is active");
		}
		else
			SendReplyError ("no nickname has been set");
	}

	void BOBCommandSession::LookupCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: lookup ", operand);
		if (*operand)
		{
			auto addr = context.GetAddressBook ().GetAddress (operand);
			if (!addr)
			{
				SendReplyError ("Address Not found");
				return;
			}
			auto localDestination = (m_CurrentDestination && m_CurrentDestination->IsRunning ()) ?
				m_CurrentDestination->GetLocalDestination () : i2p::client::context.GetSharedLocalDestination ();
			if (!localDestination)
			{
				SendReplyError ("No local destination");
				return;
			}
			if (addr->IsIdentHash ())
			{
				// we might have leaseset already
				auto leaseSet = localDestination->FindLeaseSet (addr->identHash);
				if (leaseSet)
				{
					SendReplyOK (leaseSet->GetIdentity ()->ToBase64 ());
					return;
				}
			}
			// trying to request
			auto s = shared_from_this ();
			auto requstCallback = [s](std::shared_ptr<i2p::data::LeaseSet> ls)
				{
					if (ls)
						s->SendReplyOK (ls->GetIdentity ()->ToBase64 ());
					else
						s->SendReplyError ("LeaseSet Not found");
				};
			if (addr->IsIdentHash ())
				localDestination->RequestDestination (addr->identHash, requstCallback);
			else
				localDestination->RequestDestinationWithEncryptedLeaseSet (addr->blindedPublicKey, requstCallback);
		}
		else
			SendReplyError ("empty lookup address");
	}

	void BOBCommandSession::LookupLocalCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: lookup local ", operand);
		if (*operand)
		{
			auto addr = context.GetAddressBook ().GetAddress (operand);
			if (!addr)
			{
				SendReplyError ("Address Not found");
				return;
			}
			auto ls = i2p::data::netdb.FindLeaseSet (addr->identHash);
			if (ls)
				SendReplyOK (ls->GetIdentity ()->ToBase64 ());
			else
				SendReplyError ("Local LeaseSet Not found");
		}
		else
			SendReplyError ("empty lookup address");
	}

	void BOBCommandSession::PingCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: ping ", operand);
		if (*operand)
		{
			auto addr = context.GetAddressBook ().GetAddress (operand);
			if (!addr)
			{
				SendReplyError ("Address Not found");
				return;
			}
			auto localDestination = (m_CurrentDestination && m_CurrentDestination->IsRunning ()) ?
				m_CurrentDestination->GetLocalDestination () : i2p::client::context.GetSharedLocalDestination ();
			if (!localDestination)
			{
				SendReplyError ("No local destination");
				return;
			}
			if (addr->IsIdentHash ())
			{
				// we might have leaseset already
				auto leaseSet = localDestination->FindLeaseSet (addr->identHash);
				if (leaseSet)
				{
					boost::asio::post (localDestination->GetService (),
						std::bind (&BOBCommandSession::SendPing, shared_from_this (), leaseSet));
					return;
				}
			}
			// request LeaseSet
			if (addr->IsIdentHash ())
				localDestination->RequestDestination (addr->identHash,
					std::bind (&BOBCommandSession::SendPing, shared_from_this (), std::placeholders::_1));
			else
				localDestination->RequestDestinationWithEncryptedLeaseSet (addr->blindedPublicKey,
					std::bind (&BOBCommandSession::SendPing, shared_from_this (), std::placeholders::_1));
		}
		else
			SendReplyError ("empty ping address");
	}

	void BOBCommandSession::SendPing (std::shared_ptr<const i2p::data::LeaseSet> ls)
	{
		if (!ls)
		{
			SendReplyError ("LeaseSet Not found");
			return;
		}
		auto localDestination = (m_CurrentDestination && m_CurrentDestination->IsRunning ()) ?
				m_CurrentDestination->GetLocalDestination () : i2p::client::context.GetSharedLocalDestination ();
		if (!localDestination)
		{
			SendReplyError ("No local destination");
			return;
		}
		auto streamingDestination = localDestination->GetStreamingDestination ();
		if (!streamingDestination)
		{
			SendReplyError ("No streaming destination");
			return;
		}
		auto timer = std::make_shared<boost::asio::steady_timer>(localDestination->GetService ());
		timer->expires_after (std::chrono::milliseconds(BOB_PING_TIMEOUT));
		timer->async_wait ([streamingDestination, s = shared_from_this ()](const boost::system::error_code& ecode)
		{
			if (ecode != boost::asio::error::operation_aborted)
			{
				LogPrint (eLogDebug, "BOB: Pong not received after ", BOB_PING_TIMEOUT, " millliseconds");
				streamingDestination->ResetPongHandler ();
				s->SendReplyError ("timeout");
			}
		});
		auto ts = i2p::util::GetMillisecondsSinceEpoch ();
		streamingDestination->SetPongHandler (
		    [streamingDestination, timer, ts, s = shared_from_this ()](const i2p::data::IdentHash * from)
			{
				int t = i2p::util::GetMillisecondsSinceEpoch () - ts;
				if (t < 0) t = 0;
				streamingDestination->ResetPongHandler ();
				timer->cancel ();
				if (from)
					s->SendReplyOK ({"pong ", "from ", from->ToBase32(), ".b32.i2p: time=", std::to_string (t), " ms"});
				else
					s->SendReplyOK ({"pong: time=", std::to_string (t), " ms"});
			});
		streamingDestination->SendPing (ls);
	}

	void BOBCommandSession::ClearCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: clear");
		m_Owner.DeleteDestination (m_Nickname);
		m_Nickname = "";
		SendReplyOK ("cleared");
	}

	void BOBCommandSession::ListCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: list");
		std::string statusLine;
		bool sentCurrent = false;
		const auto& destinations = m_Owner.GetDestinations ();
		for (const auto& it: destinations)
		{
			BuildStatusLine(false, it.second, statusLine);
			SendRaw(statusLine);
			if(m_Nickname.compare(it.second->GetNickname()) == 0)
				sentCurrent = true;
		}
		if(!sentCurrent && !m_Nickname.empty())
		{
			// add the current tunnel to the list.
			// this is for the incomplete tunnel which has not been started yet.
			BuildStatusLine(true, m_CurrentDestination, statusLine);
			SendRaw(statusLine);
		}
		SendReplyOK ("Listing done");
	}

	void BOBCommandSession::OptionCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: option ", operand);
		const char * value = strchr (operand, '=');
		if (value)
		{
			*(const_cast<char *>(value)) = 0;
			m_Options.Insert (operand, value + 1);
			SendReplyOK ({ "option ", operand, " set to ", value + 1 });
			*(const_cast<char *>(value)) = '=';
		}
		else
			SendReplyError ("malformed");
	}

	void BOBCommandSession::StatusCommandHandler (const char * operand, size_t len)
	{
		LogPrint (eLogDebug, "BOB: status ", operand);
		const std::string name = operand;
		std::string statusLine;

		// always prefer destination
		auto dest = m_Owner.FindDestination(name);
		if(dest)
		{
			// tunnel destination exists
			BuildStatusLine(false, dest, statusLine);
			SendReplyOK(statusLine);
		}
		else
		{
			if(m_Nickname == name && !name.empty())
			{
				// tunnel is incomplete / has not been started yet
				BuildStatusLine(true, nullptr, statusLine);
				SendReplyOK(statusLine);
			}
			else
			{
				SendReplyError("no nickname has been set");
			}
		}
	}
	void BOBCommandSession::HelpCommandHandler (const char * operand, size_t len)
	{
		auto helpStrings = m_Owner.GetHelpStrings();
		if(!*operand)
		{
			std::stringstream ss;
			ss << "COMMANDS:";
			for (auto const& x : helpStrings)
			{
				ss << " " << x.first;
			}
			SendReplyOK(ss.str ());
		}
		else
		{
			auto it = helpStrings.find(operand);
			if (it != helpStrings.end ())
			{
				SendReplyOK(it->second);
				return;
			}
			SendReplyError("No such command");
		}
	}

	void BOBCommandSession::SetTunnelTypeCommandHandler (const char * operand, size_t len)
	{
		std::string_view sv(operand, len);
		LogPrint (eLogDebug, "BOB: settunneltype ", operand);
			if (sv == "socks")
			{
				m_tunnelType = TunnelType::SOCKS;
				SendReplyOK ("tunnel type set to SOCKS");
			}
			else if (sv == "httpproxy")
			{
				m_tunnelType = TunnelType::HTTP_PROXY;
				SendReplyOK ("tunnel type set to HTTP proxy");
			}
			else
			{
				m_tunnelType.reset();
				SendReplyError ("no tunnel type has been set");
			}
	}

	BOBCommandChannel::BOBCommandChannel (const std::string& address, uint16_t port):
		RunnableService ("BOB"),
		m_Acceptor (GetIOService (), boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(address), port))
	{
		// command -> handler
		m_CommandHandlers[BOB_COMMAND_ZAP] = &BOBCommandSession::ZapCommandHandler;
		m_CommandHandlers[BOB_COMMAND_QUIT] = &BOBCommandSession::QuitCommandHandler;
		m_CommandHandlers[BOB_COMMAND_START] = &BOBCommandSession::StartCommandHandler;
		m_CommandHandlers[BOB_COMMAND_STOP] = &BOBCommandSession::StopCommandHandler;
		m_CommandHandlers[BOB_COMMAND_SETNICK] = &BOBCommandSession::SetNickCommandHandler;
		m_CommandHandlers[BOB_COMMAND_GETNICK] = &BOBCommandSession::GetNickCommandHandler;
		m_CommandHandlers[BOB_COMMAND_NEWKEYS] = &BOBCommandSession::NewkeysCommandHandler;
		m_CommandHandlers[BOB_COMMAND_GETKEYS] = &BOBCommandSession::GetkeysCommandHandler;
		m_CommandHandlers[BOB_COMMAND_SETKEYS] = &BOBCommandSession::SetkeysCommandHandler;
		m_CommandHandlers[BOB_COMMAND_GETDEST] = &BOBCommandSession::GetdestCommandHandler;
		m_CommandHandlers[BOB_COMMAND_OUTHOST] = &BOBCommandSession::OuthostCommandHandler;
		m_CommandHandlers[BOB_COMMAND_OUTPORT] = &BOBCommandSession::OutportCommandHandler;
		m_CommandHandlers[BOB_COMMAND_INHOST] = &BOBCommandSession::InhostCommandHandler;
		m_CommandHandlers[BOB_COMMAND_INPORT] = &BOBCommandSession::InportCommandHandler;
		m_CommandHandlers[BOB_COMMAND_QUIET] = &BOBCommandSession::QuietCommandHandler;
		m_CommandHandlers[BOB_COMMAND_LOOKUP] = &BOBCommandSession::LookupCommandHandler;
		m_CommandHandlers[BOB_COMMAND_LOOKUP_LOCAL] = &BOBCommandSession::LookupLocalCommandHandler;
		m_CommandHandlers[BOB_COMMAND_PING] = &BOBCommandSession::PingCommandHandler;
		m_CommandHandlers[BOB_COMMAND_CLEAR] = &BOBCommandSession::ClearCommandHandler;
		m_CommandHandlers[BOB_COMMAND_LIST] = &BOBCommandSession::ListCommandHandler;
		m_CommandHandlers[BOB_COMMAND_OPTION] = &BOBCommandSession::OptionCommandHandler;
		m_CommandHandlers[BOB_COMMAND_STATUS] = &BOBCommandSession::StatusCommandHandler;
		m_CommandHandlers[BOB_COMMAND_HELP] = &BOBCommandSession::HelpCommandHandler;
		m_CommandHandlers[BOB_COMMAND_SETTUNNELTYPE] = &BOBCommandSession::SetTunnelTypeCommandHandler;
		// command -> help string
		m_HelpStrings[BOB_COMMAND_ZAP] = BOB_HELP_ZAP;
		m_HelpStrings[BOB_COMMAND_QUIT] = BOB_HELP_QUIT;
		m_HelpStrings[BOB_COMMAND_START] = BOB_HELP_START;
		m_HelpStrings[BOB_COMMAND_STOP] = BOB_HELP_STOP;
		m_HelpStrings[BOB_COMMAND_SETNICK] = BOB_HELP_SETNICK;
		m_HelpStrings[BOB_COMMAND_GETNICK] = BOB_HELP_GETNICK;
		m_HelpStrings[BOB_COMMAND_NEWKEYS] = BOB_HELP_NEWKEYS;
		m_HelpStrings[BOB_COMMAND_GETKEYS] = BOB_HELP_GETKEYS;
		m_HelpStrings[BOB_COMMAND_SETKEYS] = BOB_HELP_SETKEYS;
		m_HelpStrings[BOB_COMMAND_GETDEST] = BOB_HELP_GETDEST;
		m_HelpStrings[BOB_COMMAND_OUTHOST] = BOB_HELP_OUTHOST;
		m_HelpStrings[BOB_COMMAND_OUTPORT] = BOB_HELP_OUTPORT;
		m_HelpStrings[BOB_COMMAND_INHOST] = BOB_HELP_INHOST;
		m_HelpStrings[BOB_COMMAND_INPORT] = BOB_HELP_INPORT;
		m_HelpStrings[BOB_COMMAND_QUIET] = BOB_HELP_QUIET;
		m_HelpStrings[BOB_COMMAND_LOOKUP] = BOB_HELP_LOOKUP;
		m_HelpStrings[BOB_COMMAND_LOOKUP_LOCAL] = BOB_HELP_LOOKUP_LOCAL;
		m_HelpStrings[BOB_COMMAND_CLEAR] = BOB_HELP_CLEAR;
		m_HelpStrings[BOB_COMMAND_LIST] = BOB_HELP_LIST;
		m_HelpStrings[BOB_COMMAND_OPTION] = BOB_HELP_OPTION;
		m_HelpStrings[BOB_COMMAND_STATUS] = BOB_HELP_STATUS;
		m_HelpStrings[BOB_COMMAND_SETTUNNELTYPE] = BOB_HELP_SETTUNNELTYPE;
		m_HelpStrings[BOB_COMMAND_PING] = BOB_HELP_PING;
		m_HelpStrings[BOB_COMMAND_HELP] = BOB_HELP_HELP;
	}

	BOBCommandChannel::~BOBCommandChannel ()
	{
		if (IsRunning ())
			Stop ();
	}

	void BOBCommandChannel::Start ()
	{
		Accept ();
		StartIOService ();
	}

	void BOBCommandChannel::Stop ()
	{
		for (auto& it: m_Destinations)
			it.second->Stop ();
		m_Acceptor.cancel ();
		StopIOService ();
	}

	void BOBCommandChannel::AddDestination (const std::string& name, std::shared_ptr<BOBDestination> dest)
	{
		m_Destinations.emplace (name, dest);
	}

	void BOBCommandChannel::DeleteDestination (const std::string& name)
	{
		auto it = m_Destinations.find (name);
		if (it != m_Destinations.end ())
		{
			it->second->Stop ();
			m_Destinations.erase (it);
		}
	}

	std::shared_ptr<BOBDestination> BOBCommandChannel::FindDestination (const std::string& name)
	{
		auto it = m_Destinations.find (name);
		if (it != m_Destinations.end ())
			return it->second;
		return nullptr;
	}

	void BOBCommandChannel::SetProxy (const std::string& name, std::unique_ptr<I2PService> proxy)
	{
		m_proxy[name] = std::move(proxy);
	}

	const I2PService* BOBCommandChannel::GetProxy(const std::string& name) const
	{
		auto it = m_proxy.find(name);
		if (it != m_proxy.end() && it->second)
			return it->second.get();
		return nullptr;
	}

	void BOBCommandChannel::RemoveProxy(const std::string& name)
	{
		auto it = m_proxy.find (name);
		if (it != m_proxy.end ())
		{
			m_proxy.erase (it);
		}
	}

	void BOBCommandChannel::Accept ()
	{
		auto newSession = std::make_shared<BOBCommandSession> (*this);
		m_Acceptor.async_accept (newSession->GetSocket (), std::bind (&BOBCommandChannel::HandleAccept, this,
			std::placeholders::_1, newSession));
	}

	void BOBCommandChannel::HandleAccept(const boost::system::error_code& ecode, std::shared_ptr<BOBCommandSession> session)
	{
		if (ecode != boost::asio::error::operation_aborted)
			Accept ();

		if (!ecode)
		{
			LogPrint (eLogInfo, "BOB: New command connection from ", session->GetSocket ().remote_endpoint ());
			session->SendVersion ();
		}
		else
			LogPrint (eLogError, "BOB: Accept error: ", ecode.message ());
	}
}
}
