Commit d622d507 authored by Tomek Mrugalski's avatar Tomek Mrugalski 🛰
Browse files

[3880] Changes after review:

 - CommandSocket, UNIXCommandSocket and ConnectionSocket classes added.
 - Updated unit-test to use
parent fcef89cb
......@@ -18,6 +18,7 @@ lib_LTLIBRARIES = libkea-cfgclient.la
libkea_cfgclient_la_SOURCES = config_data.h config_data.cc
libkea_cfgclient_la_SOURCES += module_spec.h module_spec.cc
libkea_cfgclient_la_SOURCES += command_mgr.cc command_mgr.h
libkea_cfgclient_la_SOURCES += command_socket.cc command_socket.h
libkea_cfgclient_la_SOURCES += command_socket_factory.cc command_socket_factory.h
libkea_cfgclient_la_SOURCES += config_log.h config_log.cc
......
......@@ -30,30 +30,52 @@ CommandMgr::CommandMgr() {
boost::bind(&CommandMgr::listCommandsHandler, this, _1, _2));
}
int CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
if (socket_info_) {
CommandSocketPtr
CommandMgr::openCommandSocket(const isc::data::ConstElementPtr& socket_info) {
if (socket_) {
isc_throw(SocketError, "There is already a control socket open");
}
socket_ = CommandSocketFactory::create(socket_info);
socket_info_ = socket_info;
// Install this socket in Interface Manager.
isc::dhcp::IfaceMgr::instance().addExternalSocket(socket_,
boost::bind(&isc::config::CommandMgr::connectionAcceptor, socket_));
return (socket_);
}
void CommandMgr::closeCommandSocket() {
if (socket_info_) {
// First, let's close the socket for incoming new connections.
if (socket_) {
socket_->close();
socket_.reset();
}
// Now let's close all existing connections that we may have.
for (std::list<CommandSocketPtr>::iterator conn = connections_.begin();
conn != connections_.end(); ++conn) {
(*conn)->close();
}
connections_.clear();
}
isc::dhcp::IfaceMgr::instance().deleteExternalSocket(socket_);
void CommandMgr::addConnection(const CommandSocketPtr& conn) {
connections_.push_back(conn);
}
bool CommandMgr::closeConnection(int fd) {
// Let's iterate over all currently registered connections.
for (std::list<CommandSocketPtr>::iterator conn = connections_.begin();
conn != connections_.end(); ++conn) {
CommandSocketFactory::close(socket_, socket_info_);
socket_ = 0;
socket_info_.reset();
// If found, close it.
if ((*conn)->getFD() == fd) {
(*conn)->close();
connections_.erase(conn);
return (true);
}
}
return (false);
}
CommandMgr&
......@@ -104,35 +126,6 @@ void CommandMgr::deregisterAll() {
boost::bind(&CommandMgr::listCommandsHandler, this, _1, _2));
}
void
CommandMgr::connectionAcceptor(int sockfd) {
/// @todo: Either make this generic or rename this method
/// to CommandSocketFactory::unixConnectionAcceptor
struct sockaddr_un client_addr;
socklen_t client_addr_len;
client_addr_len = sizeof(client_addr);
// Accept incoming connection. This will create a separate socket for
// handling this specific connection.
int fd2 = accept(sockfd, (struct sockaddr*) &client_addr, &client_addr_len);
// Not sure if this is really needed, but let's set it to non-blocking mode.
fcntl(fd2, F_SETFL, O_NONBLOCK);
// Install commandReader callback. When there's any data incoming on this
// socket, commandReader will be called and process it. It may also
// eventually close this socket.
isc::dhcp::IfaceMgr::instance().addExternalSocket(fd2,
boost::bind(&isc::config::CommandMgr::commandReader, fd2));
// Remember this socket descriptor. It will be needed when we shut down the
// server.
instance().connections_.push_back(fd2);
LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_OPENED).arg(fd2).arg(sockfd);
}
void
CommandMgr::commandReader(int sockfd) {
......@@ -152,19 +145,8 @@ CommandMgr::commandReader(int sockfd) {
return;
} else if (rval == 0) {
// read of 0 bytes means end-of-file. In other words the connection is
// being closed.
LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_CLOSED).arg(sockfd);
// Unregister this callback
isc::dhcp::IfaceMgr::instance().deleteExternalSocket(sockfd);
// Close the socket.
close(sockfd);
// Remove it from the active connections list.
instance().connections_.remove(sockfd);
instance().closeConnection(sockfd);
return;
}
......
......@@ -16,6 +16,7 @@
#define COMMAND_MGR_H
#include <cc/data.h>
#include <config/command_socket.h>
#include <boost/noncopyable.hpp>
#include <boost/function.hpp>
#include <string>
......@@ -98,8 +99,9 @@ public:
/// @throw SocketError if command socket is already open.
///
/// @param socket_info describes control socket parameters
/// @return socket descriptor of the socket created
int openCommandSocket(const isc::data::ConstElementPtr& socket_info);
/// @return object representing a socket
CommandSocketPtr
openCommandSocket(const isc::data::ConstElementPtr& socket_info);
/// @brief Shuts down any open control sockets
void closeCommandSocket();
......@@ -124,16 +126,6 @@ public:
/// it may be called by external code explicitly. Hence this method is public.
isc::data::ConstElementPtr processCommand(const isc::data::ConstElementPtr& cmd);
/// @brief Callback used to accept incoming connections.
///
/// This callback is used on a control socket. Once called, it will accept
/// incoming connection, create new socket for it and install @ref commandReader
/// for that new socket in @ref isc::dhcp::IfaceMgr.
///
/// @param sockfd socket descriptor of a socket capable of accepting
/// incoming connections
static void connectionAcceptor(int sockfd);
/// @brief Reads data from a socket, parses as JSON command and processes it
///
/// This method is used to handle traffic on connected socket. This callback
......@@ -150,12 +142,22 @@ public:
/// handled at all times.
void deregisterAll();
/// @brief Adds an information about opened connection socket
///
/// @param conn Connection socket to be stored
void addConnection(const CommandSocketPtr& conn);
/// @brief Closes connection with a specific socket descriptor
///
/// @param fd socket descriptor
/// @return true if closed successfully, false if not found
bool closeConnection(int fd);
/// @brief Returns control socket descriptor
///
/// This method should be used only in tests.
int getControlSocketFD() const {
return (socket_);
return (socket_->getFD());
}
private:
......@@ -181,14 +183,17 @@ private:
/// @brief Container for command handlers
HandlerContainer handlers_;
/// @brief Socket file descriptor
int socket_;
/// @brief Parameters for control socket
isc::data::ConstElementPtr socket_info_;
/// @brief Control socket structure
///
/// This is the socket that accepts incoming connections. There can be at
/// most one (if command channel is configured).
CommandSocketPtr socket_;
/// @brief Socket descriptors for open connections
std::list<int> connections_;
/// @brief Sockets for open connections
///
/// These are the sockets that are dedicated to handle a specific connection.
/// Their number is equal to number of current control connections.
std::list<CommandSocketPtr> connections_;
};
}; // end of isc::config namespace
......
// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <config/command_socket.h>
#include <config/command_mgr.h>
#include <config/config_log.h>
#include <dhcp/iface_mgr.h>
#include <boost/bind.hpp>
#include <unistd.h>
namespace isc {
namespace config {
ConnectionSocket::ConnectionSocket(int sockfd)
:CommandSocket(isc::data::ConstElementPtr()) {
sockfd_ = sockfd;
// Install commandReader callback. When there's any data incoming on this
// socket, commandReader will be called and process it. It may also
// eventually close this socket.
isc::dhcp::IfaceMgr::instance().addExternalSocket(sockfd,
boost::bind(&ConnectionSocket::receiveHandler, this));
}
void ConnectionSocket::close() {
LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_CLOSED).arg(sockfd_);
// Unregister this callback
isc::dhcp::IfaceMgr::instance().deleteExternalSocket(sockfd_);
// We're closing a connection, not the whole socket. It's ok to just
// close the connection and don't delete anything.
::close(sockfd_);
}
void ConnectionSocket::receiveHandler() {
CommandMgr::instance().commandReader(sockfd_);
}
};
};
// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#ifndef COMMAND_SOCKET_H
#define COMMAND_SOCKET_H
#include <cc/data.h>
#include <unistd.h>
namespace isc {
namespace config {
/// @brief An exception indicating that specified socket parameters are invalid
class BadSocketInfo : public Exception {
public:
BadSocketInfo(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) { };
};
/// @brief An exception indicating a problem with socket operation
class SocketError : public Exception {
public:
SocketError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) { };
};
/// @brief Abstract base class that represents an open command socket
///
/// This class is not expected to be instantiated directly. Derived classes
/// are expected to handle specific socket types (e.g. UNIX or https).
///
/// For derived classes, see @ref UnixCommandSocket for a socket that
/// accepts connections over UNIX socket and @ref ConnectionSocket that
/// handles established connections (currently over UNIX sockets, but
/// should be generic).
class CommandSocket {
public:
/// @brief Default constructor
///
/// @param socket_info socket information from the config
CommandSocket(const isc::data::ConstElementPtr& socket_info)
:socket_info_(socket_info) {
}
/// @brief Method used to handle incoming data
///
/// This may be registered in @ref isc::dhcp::IfaceMgr
virtual void receiveHandler() = 0;
/// @brief General method for closing socket.
///
/// This is the default implementation that simply closes
/// the socket. Derived classes may do additional steps
/// to terminate the connection.
virtual void close() {
::close(sockfd_);
}
/// @brief Virtual destructor.
virtual ~CommandSocket() {
close();
}
/// @brief Returns socket descriptor.
int getFD() const {
return (sockfd_);
}
protected:
/// Stores socket descriptor.
int sockfd_;
/// Stores socket information.
isc::data::ConstElementPtr socket_info_;
};
/// Pointer to a command socket object
typedef boost::shared_ptr<CommandSocket> CommandSocketPtr;
/// @brief This class represents a straming socket for handling connections
///
/// Initially a socket (e.g. UNIX) is opened (represented by other classes, e.g.
/// @ref UnixCommandSocket). Once incoming connection is detected, that class
/// calls accept(), which returns a new socket dedicated to handling that
/// specific connection. That socket is represented by this class.
class ConnectionSocket : public CommandSocket {
public:
/// @brief Default constructor
///
/// This constructor is used in methods that call accept on existing
/// sockets. accept() returns a socket descriptor. Hence only one
/// parameter here.
///
/// @param sockfd socket descriptor
ConnectionSocket(int sockfd);
/// @brief Method used to handle incoming data
///
/// This method calls isc::config::CommandMgr::commandReader method.
virtual void receiveHandler();
/// @brief Closes socket.
///
/// This method closes the socket, prints appropriate log message and
/// unregisters callback from @ref isc::dhcp::IfaceMgr.
virtual void close();
};
};
};
#endif
......@@ -14,98 +14,154 @@
#include <config/command_socket_factory.h>
#include <config/config_log.h>
#include <cstdio>
#include <config/command_mgr.h>
#include <dhcp/iface_mgr.h>
#include <boost/bind.hpp>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <unistd.h>
#include <cstdio>
#include <fcntl.h>
using namespace isc::config;
namespace {
int createUnixSocket(const std::string& file_name) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) {
isc_throw(isc::config::SocketError, "Failed to create AF_UNIX socket.");
}
// Let's remove the old file. We don't care about any possible
// errors here. The file should not be there if the file was
// shut down properly.
remove(file_name.c_str());
// Set this socket to be non-blocking one.
fcntl(fd, F_SETFL, O_NONBLOCK);
// Now bind the socket to the specified path.
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, file_name.c_str(), sizeof(addr.sun_path)-1);
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr))) {
isc_throw(isc::config::SocketError, "Failed to bind socket " << fd
<< " to " << file_name);
}
// One means that we allow at most 1 awaiting connections.
// Any additional attempts will get ECONNREFUSED error.
// That means that at any given time, there may be at most one controlling
// connection.
/// @todo: Make this a configurable.
int status = listen(fd, 1);
if (status < 0) {
isc_throw(isc::config::SocketError, "Failed to listen on socket fd="
<< fd << ", filename=" << file_name);
}
LOG_INFO(command_logger, COMMAND_SOCKET_UNIX_OPEN).arg(fd).arg(file_name);
return (fd);
}
void closeUnixSocket(int socket_fd, const std::string& file_name) {
LOG_INFO(command_logger, COMMAND_SOCKET_UNIX_CLOSE).arg(socket_fd).arg(file_name);
close(socket_fd);
unlink(file_name.c_str());
}
}
using namespace isc::data;
namespace isc {
namespace config {
int
CommandSocketFactory::create(const isc::data::ConstElementPtr& socket_info) {
if(!socket_info) {
isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket.");
/// @brief Wrapper for UNIX stream sockets
///
/// There are two UNIX socket types: datagram-based (equivalent of UDP) and
/// stream-based (equivalent of TCP). This class represents stream-based
/// sockets. It opens up a unix-socket and waits for incoming connections.
/// Once incoming connection is detected, accept() system call is called
/// and a new socket for that particular connection is returned. A new
/// object of @ref ConnectionSocket is created.
class UnixCommandSocket : public CommandSocket {
public:
/// @brief Default constructor
///
/// This socket_info map is expected to have at least one parameter:
/// 'socket-name' that specifies UNIX socket path. It's typically
/// a file somewhere in /tmp or /var/run/kea directory.
///
/// @param socket_info socket description from the configuration
UnixCommandSocket(const isc::data::ConstElementPtr& socket_info)
: CommandSocket(socket_info) {
ConstElementPtr name = socket_info->get("socket-name");
if (!name) {
isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing");
}
filename_ = name->stringValue();
sockfd_ = createUnixSocket(filename_);
// Install this socket in Interface Manager.
isc::dhcp::IfaceMgr::instance().addExternalSocket(sockfd_,
boost::bind(&UnixCommandSocket::receiveHandler, this));
}
ConstElementPtr type = socket_info->get("socket-type");
if (!type) {
isc_throw(BadSocketInfo, "Mandatory 'socket-type' parameter missing");
private:
/// @brief Auxiliary method for creating a UNIX socket
///
/// @param file_name specifies socket file path
int createUnixSocket(const std::string& file_name) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) {
isc_throw(isc::config::SocketError, "Failed to create AF_UNIX socket.");
}
// Let's remove the old file. We don't care about any possible
// errors here. The file should not be there if the file was
// shut down properly.
remove(file_name.c_str());
// Set this socket to be non-blocking one.
fcntl(fd, F_SETFL, O_NONBLOCK);
// Now bind the socket to the specified path.
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, file_name.c_str(), sizeof(addr.sun_path)-1);
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr))) {
::close(fd);
remove(file_name.c_str());
isc_throw(isc::config::SocketError, "Failed to bind socket " << fd
<< " to " << file_name);
}
// One means that we allow at most 1 awaiting connections.
// Any additional attempts will get ECONNREFUSED error.
// That means that at any given time, there may be at most one controlling
// connection.
/// @todo: Make the number of parallel connections configurable.
int status = listen(fd, 1);
if (status < 0) {
::close(fd);
remove(file_name.c_str());
isc_throw(isc::config::SocketError, "Failed to listen on socket fd="
<< fd << ", filename=" << file_name);
}
// Woohoo! Socket opened, let's log it!
LOG_INFO(command_logger, COMMAND_SOCKET_UNIX_OPEN).arg(fd).arg(file_name);
return (fd);
}
ConstElementPtr name = socket_info->get("socket-name");
if (!name) {
isc_throw(BadSocketInfo, "Mandatory 'socket-name' parameter missing");
/// CommandMgr::connectionAcceptor(int sockfd) {
/// @brief Callback used to accept incoming connections.
///
/// This callback is used on a control socket. Once called, it will accept
/// incoming connection, create new socket for it and install
/// @ref commandReader for that new socket in @ref isc::dhcp::IfaceMgr.
void receiveHandler() {
// This method is specific to receiving data over UNIX socket, so using
// sockaddr_un instead of sockaddr_storage here is ok.
struct sockaddr_un client_addr;
socklen_t client_addr_len;
client_addr_len = sizeof(client_addr);
// Accept incoming connection. This will create a separate socket for
// handling this specific connection.
int fd2 = accept(sockfd_, static_cast<struct sockaddr*>(&client_addr),
&client_addr_len);
// And now create an object that represents that new connection.
CommandSocketPtr conn(new ConnectionSocket(fd2));
// Not sure if this is really needed, but let's set it to non-blocking
// mode.
fcntl(fd2, F_SETFL, O_NONBLOCK);
// Remember this socket descriptor. It will be needed when we shut down
// the server.
CommandMgr::instance().addConnection(conn);
LOG_INFO(command_logger, COMMAND_SOCKET_CONNECTION_OPENED).arg(fd2)
.arg(sockfd_);
}
if (type->stringValue() == "unix") {
return (createUnixSocket(name->stringValue()));
} else {
isc_throw(BadSocketInfo, "Specified socket type ('" + type->stringValue()
+ "') is not supported.");
// This method is called when we shutdown the connection.
void close() {
LOG_INFO(command_logger, COMMAND_SOCKET_UNIX_CLOSE).arg(sockfd_)
.arg(filename_);
isc::dhcp::IfaceMgr::instance().deleteExternalSocket(sockfd_);
::close(sockfd_);
remove(filename_.c_str());
}
}
void CommandSocketFactory::close(int socket_fd,
const isc::data::ConstElementPtr& socket_info) {
std::string filename_;
};
CommandSocketPtr
CommandSocketFactory::create(const isc::data::ConstElementPtr& socket_info) {
if(!socket_info) {
isc_throw(BadSocketInfo, "Missing socket_info parameters, can't create socket.");
}
......@@ -115,13 +171,8 @@ void CommandSocketFactory::close(int socket_fd,