|
|
|
/*
|
|
|
|
* Copyright (c) 2013-2022, 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
|
|
|
|
{
|
|
|
|
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>(this, receiver->socket, leaseSet);
|
|
|
|
AddHandler (connection);
|
|
|
|
connection->I2PConnect (receiver->data, receiver->dataLen);
|
|
|
|
}
|
|
|
|
|
|
|
|
BOBI2POutboundTunnel::BOBI2POutboundTunnel (const std::string& outhost, int port,
|
|
|
|
std::shared_ptr<ClientDestination> localDestination, bool quiet): BOBI2PTunnel (localDestination),
|
|
|
|
m_Endpoint (boost::asio::ip::address::from_string (outhost), port), m_IsQuiet (quiet)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBI2POutboundTunnel::Start ()
|
|
|
|
{
|
|
|
|
Accept ();
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBI2POutboundTunnel::Stop ()
|
|
|
|
{
|
|
|
|
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<I2PTunnelConnection> (this, stream, std::make_shared<boost::asio::ip::tcp::socket> (GetService ()), 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 int inport, const int 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 ()
|
|
|
|
{
|
|
|
|
delete m_OutboundTunnel;
|
|
|
|
delete m_InboundTunnel;
|
|
|
|
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 ();
|
|
|
|
delete m_OutboundTunnel;
|
|
|
|
m_OutboundTunnel = nullptr;
|
|
|
|
}
|
|
|
|
if (m_InboundTunnel)
|
|
|
|
{
|
|
|
|
m_InboundTunnel->Stop ();
|
|
|
|
delete m_InboundTunnel;
|
|
|
|
m_InboundTunnel = nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBDestination::CreateInboundTunnel (int 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::address::from_string (inhost, ec);
|
|
|
|
if (!ec)
|
|
|
|
ep.address (addr);
|
|
|
|
else
|
|
|
|
LogPrint (eLogError, "BOB: ", ec.message ());
|
|
|
|
}
|
|
|
|
m_InboundTunnel = new BOBI2PInboundTunnel (ep, m_LocalDestination);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBDestination::CreateOutboundTunnel (const std::string& outhost, int port, bool quiet)
|
|
|
|
{
|
|
|
|
if (!m_OutboundTunnel)
|
|
|
|
{
|
|
|
|
// update outport and outhost (user can stop tunnel and change)
|
|
|
|
m_OutPort = port;
|
|
|
|
m_OutHost = outhost;
|
|
|
|
m_OutboundTunnel = new 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 ()
|
|
|
|
{
|
|
|
|
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 (const char * msg)
|
|
|
|
{
|
|
|
|
std::ostream os(&m_SendBuffer);
|
|
|
|
os << "OK";
|
|
|
|
if(msg)
|
|
|
|
{
|
|
|
|
os << " " << msg;
|
|
|
|
}
|
|
|
|
os << std::endl;
|
|
|
|
Send ();
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBCommandSession::SendReplyError (const char * 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBCommandSession::SendRaw (const char * data)
|
|
|
|
{
|
|
|
|
std::ostream os(&m_SendBuffer);
|
|
|
|
os << data << std::endl;
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBCommandSession::BuildStatusLine(bool currentTunnel, 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->IsRunning(); };
|
|
|
|
const auto bool_str = [](const bool v) { return v ? "true" : "false"; }; // bool -> str
|
|
|
|
|
|
|
|
// 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); // key must exist when destination is created
|
|
|
|
const bool starting = destExists(dest) && !destReady(dest);
|
|
|
|
const bool running = destExists(dest) && destReady(dest);
|
|
|
|
const bool stopping = false;
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
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::address::from_string(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::address::from_string(m_OutHost, ec);
|
|
|
|
if (ec)
|
|
|
|
{
|
|
|
|
SendReplyError("outhost must be a IPv4 address.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!m_CurrentDestination)
|
|
|
|
{
|
|
|
|
m_CurrentDestination = new 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_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 ();
|
|
|
|
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);
|
|
|
|
if (dest)
|
|
|
|
{
|
|
|
|
dest->StopTunnels ();
|
|
|
|
SendReplyOK ("Tunnel stopping");
|
|
|
|
}
|
|
|
|
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;
|
|
|
|
std::string msg ("Nickname set to ");
|
|
|
|
msg += m_Nickname;
|
|
|
|
SendReplyOK (msg.c_str ());
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
if (m_CurrentDestination)
|
|
|
|
{
|
|
|
|
m_Keys = m_CurrentDestination->GetKeys ();
|
|
|
|
m_IsActive = m_CurrentDestination->IsRunning ();
|
|
|
|
m_Nickname = operand;
|
|
|
|
}
|
|
|
|
if (m_Nickname == operand)
|
|
|
|
{
|
|
|
|
std::string msg ("Nickname set to ");
|
|
|
|
msg += m_Nickname;
|
|
|
|
SendReplyOK (msg.c_str ());
|
|
|
|
}
|
|
|
|
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_DSA_SHA1;
|
|
|
|
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);
|
|
|
|
SendReplyOK (m_Keys.GetPublic ()->ToBase64 ().c_str ());
|
|
|
|
}
|
|
|
|
|
|
|
|
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 ().c_str ());
|
|
|
|
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 ().c_str ());
|
|
|
|
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 ().c_str ());
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
m_OutPort = std::stoi(operand);
|
|
|
|
if (m_OutPort >= 0)
|
|
|
|
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)
|
|
|
|
{
|
|
|
|
m_InPort = std::stoi(operand);
|
|
|
|
if (m_InPort >= 0)
|
|
|
|
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->GetLocalDestination () : i2p::client::context.GetSharedLocalDestination ();
|
|
|
|
if (addr->IsIdentHash ())
|
|
|
|
{
|
|
|
|
// we might have leaseset already
|
|
|
|
auto leaseSet = localDestination->FindLeaseSet (addr->identHash);
|
|
|
|
if (leaseSet)
|
|
|
|
{
|
|
|
|
SendReplyOK (leaseSet->GetIdentity ()->ToBase64 ().c_str ());
|
|
|
|
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 ().c_str ());
|
|
|
|
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 ().c_str ());
|
|
|
|
else
|
|
|
|
SendReplyError ("Local LeaseSet Not found");
|
|
|
|
}
|
|
|
|
else
|
|
|
|
SendReplyError ("empty lookup address");
|
|
|
|
}
|
|
|
|
|
|
|
|
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.c_str());
|
|
|
|
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.c_str());
|
|
|
|
}
|
|
|
|
SendReplyOK ("Listing done");
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBCommandSession::OptionCommandHandler (const char * operand, size_t len)
|
|
|
|
{
|
|
|
|
LogPrint (eLogDebug, "BOB: option ", operand);
|
|
|
|
const char * value = strchr (operand, '=');
|
|
|
|
if (value)
|
|
|
|
{
|
|
|
|
std::string msg ("option ");
|
|
|
|
*(const_cast<char *>(value)) = 0;
|
|
|
|
m_Options[operand] = value + 1;
|
|
|
|
msg += operand;
|
|
|
|
*(const_cast<char *>(value)) = '=';
|
|
|
|
msg += " set to ";
|
|
|
|
msg += value + 1;
|
|
|
|
SendReplyOK (msg.c_str ());
|
|
|
|
}
|
|
|
|
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.c_str());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if(m_Nickname == name && !name.empty())
|
|
|
|
{
|
|
|
|
// tunnel is incomplete / has not been started yet
|
|
|
|
BuildStatusLine(true, nullptr, statusLine);
|
|
|
|
SendReplyOK(statusLine.c_str());
|
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
const std::string &str = ss.str();
|
|
|
|
SendReplyOK(str.c_str());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
auto it = helpStrings.find(operand);
|
|
|
|
if (it != helpStrings.end ())
|
|
|
|
{
|
|
|
|
SendReplyOK(it->second.c_str());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
SendReplyError("No such command");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BOBCommandChannel::BOBCommandChannel (const std::string& address, int port):
|
|
|
|
RunnableService ("BOB"),
|
|
|
|
m_Acceptor (GetIOService (), boost::asio::ip::tcp::endpoint(boost::asio::ip::address::from_string(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_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;
|
|
|
|
// 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_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_HELP] = BOB_HELP_HELP;
|
|
|
|
}
|
|
|
|
|
|
|
|
BOBCommandChannel::~BOBCommandChannel ()
|
|
|
|
{
|
|
|
|
if (IsRunning ())
|
|
|
|
Stop ();
|
|
|
|
for (const auto& it: m_Destinations)
|
|
|
|
delete it.second;
|
|
|
|
}
|
|
|
|
|
|
|
|
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, BOBDestination * dest)
|
|
|
|
{
|
|
|
|
m_Destinations[name] = dest;
|
|
|
|
}
|
|
|
|
|
|
|
|
void BOBCommandChannel::DeleteDestination (const std::string& name)
|
|
|
|
{
|
|
|
|
auto it = m_Destinations.find (name);
|
|
|
|
if (it != m_Destinations.end ())
|
|
|
|
{
|
|
|
|
it->second->Stop ();
|
|
|
|
delete it->second;
|
|
|
|
m_Destinations.erase (it);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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::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 ());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|