Files
pvxs/src/serverconn.cpp
T
Michael Davidsaver 9bbe6292a2 compat notes
2022-05-18 12:27:33 -07:00

449 lines
13 KiB
C++

/**
* Copyright - See the COPYRIGHT that is included with this distribution.
* pvxs is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
*/
#include <limits>
#include <system_error>
#include <utility>
#include <osiSock.h>
#include <epicsGuard.h>
#include <epicsAssert.h>
#include <pvxs/log.h>
#include "serverconn.h"
// limit on size of TX buffer above which we suspend RX
static constexpr size_t tcp_tx_limit = 0x100000;
namespace pvxs {
namespace server {
std::set<std::string> ClientCredentials::roles() const
{
std::set<std::string> ret;
osdGetRoles(account, ret);
return ret;
}
std::ostream& operator<<(std::ostream& strm, const ClientCredentials& cred)
{
strm<<cred.method<<"/"<<cred.account<<"@"<<cred.peer;
return strm;
}
}} // namespace pvxs::server
namespace pvxs {namespace impl {
// message related to client state and errors
DEFINE_LOGGER(connsetup, "pvxs.tcp.setup");
// related to low level send/recv
DEFINE_LOGGER(connio, "pvxs.tcp.io");
DEFINE_LOGGER(remote, "pvxs.remote.log");
ServerConn::ServerConn(ServIface* iface, evutil_socket_t sock, struct sockaddr *peer, int socklen)
:ConnBase(false, iface->server->effective.sendBE(),
bufferevent_socket_new(iface->server->acceptor_loop.base, sock, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS),
SockAddr(peer))
,iface(iface)
{
log_debug_printf(connio, "Client %s connects\n", peerName.c_str());
{
auto cred(std::make_shared<server::ClientCredentials>());
cred->peer = peerName;
cred->iface = iface->name;
// paranoia placeholder prior to handle_CONNECTION_VALIDATION()
cred->method = cred->account = "anonymous";
this->cred = std::move(cred);
}
bufferevent_setcb(bev.get(), &bevReadS, &bevWriteS, &bevEventS, this);
timeval tmo(totv(iface->server->effective.tcpTimeout));
bufferevent_set_timeouts(bev.get(), &tmo, &tmo);
auto tx = bufferevent_get_output(bev.get());
std::vector<uint8_t> buf(128);
// queue connection validation message
{
VectorOutBuf M(sendBE, buf);
to_wire(M, Header{pva_ctrl_msg::SetEndian, pva_flags::Control|pva_flags::Server, 0});
auto save = M.save();
M.skip(8, __FILE__, __LINE__); // placeholder for header
auto bstart = M.save();
// serverReceiveBufferSize, not used
to_wire(M, uint32_t(0x10000));
// serverIntrospectionRegistryMaxSize, also not used
to_wire(M, uint16_t(0x7fff));
/* list given in reverse order of priority.
* Old pvAccess* was missing a "break" when looping,
* so it took the last known plugin.
*/
to_wire(M, Size{2});
to_wire(M, "anonymous");
to_wire(M, "ca");
auto bend = M.save();
FixedBuf H(sendBE, save, 8);
to_wire(H, Header{CMD_CONNECTION_VALIDATION, pva_flags::Server, uint32_t(bend-bstart)});
assert(M.good() && H.good());
if(evbuffer_add(tx, buf.data(), M.save()-buf.data()))
throw std::bad_alloc();
statTx += M.save()-buf.data();
}
if(bufferevent_enable(bev.get(), EV_READ|EV_WRITE))
throw std::logic_error("Unable to enable BEV");
}
ServerConn::~ServerConn()
{}
const std::shared_ptr<ServerChan>& ServerConn::lookupSID(uint32_t sid)
{
auto it = chanBySID.find(sid);
if(it==chanBySID.end()) {
static decltype (it->second) empty{};
return empty;
//throw std::runtime_error(SB()<<"Client "<<peerName<<" non-existent SID "<<sid);
}
return it->second;
}
void ServerConn::handle_ECHO()
{
// Client requests echo as a keep-alive check
auto tx = bufferevent_get_output(bev.get());
uint32_t len = evbuffer_get_length(segBuf.get());
to_evbuf(tx, Header{CMD_ECHO, pva_flags::Server, len}, sendBE);
auto err = evbuffer_add_buffer(tx, segBuf.get());
assert(!err);
// maybe help reduce latency
bufferevent_flush(bev.get(), EV_WRITE, BEV_FLUSH);
statTx += 8u + len;
}
static
void auth_complete(ServerConn *self, const Status& sts)
{
(void)evbuffer_drain(self->txBody.get(), evbuffer_get_length(self->txBody.get()));
{
EvOutBuf M(self->sendBE, self->txBody.get());
to_wire(M, sts);
}
self->enqueueTxBody(CMD_CONNECTION_VALIDATED);
log_debug_printf(connsetup, "%s Auth complete with %d\n", self->peerName.c_str(), sts.code);
}
void ServerConn::handle_CONNECTION_VALIDATION()
{
// Client begins (restarts?) Auth handshake
EvInBuf M(peerBE, segBuf.get(), 16);
std::string selected;
{
M.skip(4+2+2, __FILE__, __LINE__); // ignore unused buffer, introspection size, and QoS
from_wire(M, selected);
Value auth;
from_wire_type_value(M, rxRegistry, auth);
if(!M.good()) {
log_err_printf(connio, "%s:%d Client %s Truncated/Invalid ConnValid from client\n",
M.file(), M.line(), peerName.c_str());
bev.reset();
return;
} else {
log_debug_printf(connsetup, "Client %s authenticates using %s and %s\n",
peerName.c_str(), selected.c_str(),
std::string(SB()<<auth).c_str());
auto C(std::make_shared<server::ClientCredentials>(*cred));
if(selected=="ca") {
auth["user"].as<std::string>([&C, &selected](const std::string& user) {
C->method = selected;
C->account = user;
});
}
if(C->method.empty()) {
C->account = C->method = "anonymous";
}
C->raw = auth;
cred = std::move(C);
}
}
if(selected!="ca" && selected!="anonymous") {
log_debug_printf(connsetup, "Client %s selects unadvertised auth \"%s\"", peerName.c_str(), selected.c_str());
auth_complete(this, Status{Status::Error, "Client selects unadvertised auth"});
return;
} else {
log_debug_printf(connsetup, "Client %s selects auth \"%s\"\n", peerName.c_str(), selected.c_str());
}
// remainder of segBuf is payload w/ credentials
// No practical way to handle auth failure.
// So we accept all credentials, but may not grant rights.
auth_complete(this, Status{Status::Ok});
}
void ServerConn::handle_AUTHNZ()
{
// ignored (so far no auth plugin actually uses)
}
void ServerConn::handle_PUT_GET()
{}
void ServerConn::handle_CANCEL_REQUEST()
{
EvInBuf M(peerBE, segBuf.get(), 16);
uint32_t sid=0, ioid=0;
from_wire(M, sid);
from_wire(M, ioid);
if(!M.good())
throw std::runtime_error(SB()<<M.file()<<':'<<M.line()<<" Error decoding DestroyOp");
auto it = opByIOID.find(ioid);
if(it==opByIOID.end()) {
log_warn_printf(connsetup, "Client %s Cancel of non-existent Op %u\n", peerName.c_str(), unsigned(ioid));
return;
}
const auto& op = it->second;
auto chan = op->chan.lock();
if(!chan || chan->sid!=sid) {
log_err_printf(connsetup, "Client %s Cancel inconsistent Op\n", peerName.c_str());
return;
}
if(op->state==ServerOp::Executing) {
op->state = ServerOp::Idle;
if(op->onCancel)
op->onCancel();
} else {
// an allowed race
log_debug_printf(connsetup, "Client %s Cancel of non-executing Op\n", peerName.c_str());
}
}
void ServerConn::handle_DESTROY_REQUEST()
{
EvInBuf M(peerBE, segBuf.get(), 16);
uint32_t sid=0, ioid=0;
from_wire(M, sid);
from_wire(M, ioid);
if(!M.good())
throw std::runtime_error(SB()<<M.file()<<':'<<M.line()<<" Error decoding DestroyOp");
auto& chan = lookupSID(sid);
auto it = opByIOID.find(ioid);
if(!chan || it==opByIOID.end() || 1!=chan->opByIOID.erase(ioid)) {
log_debug_printf(connsetup, "Client %s can't destroy non-existent op %u:%u\n",
peerName.c_str(), unsigned(sid), unsigned(ioid));
}
if(it!=opByIOID.end()) {
auto op = it->second;
opByIOID.erase(it);
op->state = ServerOp::Dead;
if(op->onClose)
op->onClose("");
}
}
void ServerConn::handle_MESSAGE()
{
EvInBuf M(peerBE, segBuf.get(), 16);
uint32_t ioid = 0;
uint8_t mtype = 0;
std::string msg;
from_wire(M, ioid);
from_wire(M, mtype);
from_wire(M, msg);
if(!M.good())
throw std::runtime_error(SB()<<M.file()<<':'<<M.line()<<" Decode error for Message");
auto it = opByIOID.find(ioid);
if(it==opByIOID.end()) {
log_debug_printf(connsetup, "Client %s Message on non-existent ioid\n", peerName.c_str());
return;
}
auto chan = it->second->chan.lock();
Level lvl;
switch(mtype) {
case 0: lvl = Level::Info;
case 1: lvl = Level::Warn;
case 2: lvl = Level::Err;
default:lvl = Level::Crit;
}
if(remote.test(lvl))
errlogPrintf("Client %s Channel %s Remote message: %s\n",
peerName.c_str(), chan ? "<dead>" : chan->name.c_str(),
msg.c_str());
}
std::shared_ptr<ConnBase> ServerConn::self_from_this()
{
return shared_from_this();
}
void ServerConn::cleanup()
{
log_debug_printf(connsetup, "Client %s Cleanup TCP Connection\n", peerName.c_str());
iface->server->connections.erase(this);
for(auto& pair : opByIOID) {
if(pair.second->onClose)
pair.second->onClose("");
}
for(auto& pair : chanBySID) {
pair.second->state = ServerChan::Destroy;
if(pair.second->onClose) {
auto fn(std::move(pair.second->onClose));
fn("");
}
}
}
void ServerConn::bevRead()
{
ConnBase::bevRead();
if(bev) {
auto tx = bufferevent_get_output(bev.get());
if(evbuffer_get_length(tx)>=tcp_tx_limit) {
// write buffer "full". stop reading until it drains
// TODO configure
(void)bufferevent_disable(bev.get(), EV_READ);
bufferevent_setwatermark(bev.get(), EV_WRITE, tcp_tx_limit/2, 0);
log_debug_printf(connio, "%s suspend READ\n", peerName.c_str());
}
}
}
void ServerConn::bevWrite()
{
log_debug_printf(connio, "%s process backlog\n", peerName.c_str());
auto tx = bufferevent_get_output(bev.get());
// handle pending monitors
while(!backlog.empty() && evbuffer_get_length(tx)<tcp_tx_limit) {
auto fn = std::move(backlog.front());
backlog.pop_front();
fn();
}
// TODO configure
if(evbuffer_get_length(tx)<tcp_tx_limit) {
(void)bufferevent_enable(bev.get(), EV_READ);
bufferevent_setwatermark(bev.get(), EV_WRITE, 0, 0);
log_debug_printf(connio, "%s resume READ\n", peerName.c_str());
}
}
ServIface::ServIface(const SockAddr &addr, server::Server::Pvt *server, bool fallback)
:server(server)
,bind_addr(addr)
{
server->acceptor_loop.assertInLoop();
auto orig_port = bind_addr.port();
sock = evsocket(bind_addr.family(), SOCK_STREAM, 0);
if(evutil_make_listen_socket_reuseable(sock.sock))
log_warn_printf(connsetup, "Unable to make socket reusable%s", "\n");
// try to bind to requested port, then fallback to a random port
while(true) {
try {
sock.bind(bind_addr);
} catch(std::system_error& e) {
if(fallback && e.code().value()==SOCK_EADDRINUSE) {
log_debug_printf(connsetup, "Address %s in use\n", bind_addr.tostring().c_str());
bind_addr.setPort(0);
fallback = false;
continue;
}
log_err_printf(connsetup, "Bind to %s fails\n", bind_addr.tostring().c_str());
throw;
}
break;
}
name = bind_addr.tostring();
if(orig_port && bind_addr.port() != orig_port) {
log_warn_printf(connsetup, "Server unable to bind port %u, falling back to %s\n", orig_port, name.c_str());
}
// added in libevent 2.1.1
#ifndef LEV_OPT_DISABLED
# define LEV_OPT_DISABLED 0
#endif
const int backlog = 4;
listener = evlisten(evconnlistener_new(server->acceptor_loop.base, onConnS, this, LEV_OPT_DISABLED|LEV_OPT_CLOSE_ON_EXEC, backlog, sock.sock));
if(!LEV_OPT_DISABLED)
evconnlistener_disable(listener.get());
}
void ServIface::onConnS(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *peer, int socklen, void *raw)
{
auto self = static_cast<ServIface*>(raw);
try {
auto conn(std::make_shared<ServerConn>(self, sock, peer, socklen));
self->server->connections[conn.get()] = std::move(conn);
}catch(std::exception& e){
log_exc_printf(connsetup, "Interface %s Unhandled error in accept callback: %s\n", self->name.c_str(), e.what());
evutil_closesocket(sock);
}
}
ServerOp::~ServerOp() {}
}} // namespace pvxs::impl