From: JINMEI Tatuya Date: Tue, 20 Dec 2011 03:15:10 +0000 (-0800) Subject: [1452b] implemented python wrapper for socket session related classes. X-Git-Tag: perftcpdns_before_epoll~5^2~5^2~2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ec48e1a92ff03492f7eca063a8a9e243d8fb2574;p=thirdparty%2Fkea.git [1452b] implemented python wrapper for socket session related classes. --- diff --git a/configure.ac b/configure.ac index 671a9b6769..5e76dd7172 100644 --- a/configure.ac +++ b/configure.ac @@ -925,6 +925,8 @@ AC_CONFIG_FILES([Makefile src/lib/python/isc/acl/tests/Makefile src/lib/python/isc/util/Makefile src/lib/python/isc/util/tests/Makefile + src/lib/python/isc/util/io/Makefile + src/lib/python/isc/util/io/tests/Makefile src/lib/python/isc/datasrc/Makefile src/lib/python/isc/datasrc/tests/Makefile src/lib/python/isc/dns/Makefile diff --git a/src/lib/python/isc/util/Makefile.am b/src/lib/python/isc/util/Makefile.am index 140e22144d..703879a96c 100644 --- a/src/lib/python/isc/util/Makefile.am +++ b/src/lib/python/isc/util/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = . tests +SUBDIRS = . io tests python_PYTHON = __init__.py process.py socketserver_mixin.py file.py diff --git a/src/lib/python/isc/util/io/Makefile.am b/src/lib/python/isc/util/io/Makefile.am new file mode 100644 index 0000000000..6e2b2637e1 --- /dev/null +++ b/src/lib/python/isc/util/io/Makefile.am @@ -0,0 +1,41 @@ +SUBDIRS = . tests + +AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) +AM_CXXFLAGS = $(B10_CXXFLAGS) + +python_PYTHON = __init__.py +pythondir = $(PYTHON_SITEPKG_DIR)/isc/util/io + +pyexec_LTLIBRARIES = socketsession.la +pyexecdir = $(PYTHON_SITEPKG_DIR)/isc/util/io + +socketsession_la_SOURCES = socketsession_python.cc socketsession_python.h +socketsession_la_SOURCES += socketsessionforwarder_python.cc +socketsession_la_SOURCES += socketsessionforwarder_python.h +socketsession_la_SOURCES += socketsessionreceiver_python.cc +socketsession_la_SOURCES += socketsessionreceiver_python.h +socketsession_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES) +socketsession_la_LDFLAGS = $(PYTHON_LDFLAGS) +# Note: PYTHON_CXXFLAGS may have some -Wno... workaround, which must be +# placed after -Wextra defined in AM_CXXFLAGS +socketsession_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS) + +# Python prefers .so, while some OSes (specifically MacOS) use a different +# suffix for dynamic objects. -module is necessary to work this around. +socketsession_la_LDFLAGS += -module +socketsession_la_LIBADD = $(top_builddir)/src/lib/util/io/libutil_io.la +socketsession_la_LIBADD += $(PYTHON_LIB) + +# This is not installed, it helps locate the module during tests +EXTRA_DIST = __init__.py socketsession.py + +EXTRA_DIST += socketsession_inc.cc +EXTRA_DIST += socketsessionforwarder_inc.cc socketsessionreceiver_inc.cc + +CLEANFILES = __init__.pyc socketsession.pyc + +CLEANDIRS = __pycache__ + +clean-local: + rm -rf $(CLEANDIRS) diff --git a/src/lib/python/isc/util/io/__init__.py b/src/lib/python/isc/util/io/__init__.py new file mode 100644 index 0000000000..935160a7ee --- /dev/null +++ b/src/lib/python/isc/util/io/__init__.py @@ -0,0 +1,3 @@ +""" +Here are function and classes for forwarding socket sessions between processes. +""" diff --git a/src/lib/python/isc/util/io/socketsession.py b/src/lib/python/isc/util/io/socketsession.py new file mode 100644 index 0000000000..ccb16ece92 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsession.py @@ -0,0 +1,26 @@ +# Copyright (C) 2011 Internet Systems Consortium. +# +# Permission to use, copy, modify, and 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 INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM 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. + +# This file is not installed. See python/isc/log/__init__.py for the trick. + +import os +import sys + +for base in sys.path[:]: + libdir = os.path.join(base, 'isc/util/io/.libs') + if os.path.exists(libdir): + sys.path.insert(0, libdir) + +from socketsession import * diff --git a/src/lib/python/isc/util/io/socketsession_inc.cc b/src/lib/python/isc/util/io/socketsession_inc.cc new file mode 100644 index 0000000000..e20006323c --- /dev/null +++ b/src/lib/python/isc/util/io/socketsession_inc.cc @@ -0,0 +1,122 @@ +namespace { +const char* const socketsession_doc = "\ +This module defines a set of classes that support forwarding a\n\ +\"socket session\" from one process to another. A socket session is a\n\ +conceptual tuple of the following elements:\n\ +\n\ +- A network socket\n\ +- The local and remote endpoints of a (IP) communication taking place\n\ + on the socket. In practice an endpoint is a pair of an IP address\n\ + and TCP or UDP port number.\n\ +- Some amount of data sent from the remote endpoint and received on\n\ + the socket. We call it (socket) session data in this documentation.\n\ +\n\ +Note that this is a conceptual definition. Depending on the underlying\n\ +implementation and/or the network protocol, some of the elements could\n\ +be part of others; for example, if it's an established TCP connection,\n\ +the local and remote endpoints would be able to be retrieved from the\n\ +socket using the standard getsockname() and getpeername() system\n\ +calls. But in this definition we separate these to be more generic.\n\ +Also, as a matter of fact our intended usage includes non-connected\n\ +UDP communications, in which case at least the remote endpoint should\n\ +be provided separately from the socket.\n\ +\n\ +In the actual implementation we represent a socket as a Python socket\n\ +object, which contains the information of the address family\n\ +(e.g. AF_INET6), socket type (e.g. SOCK_STREAM), and protocol\n\ +(e.g. IPPROTO_TCP).\n\ +\n\ +We use the Python socket address tuple to represent endpoints.\n\ +\n\ +Socket session data is an opaque blob in the form of a Python byte\n\ +object.\n\ +\n\ +To forward a socket session between processes, we use connected UNIX\n\ +domain sockets established between the processes. The file descriptor\n\ +will be forwarded through the sockets as an ancillary data item of\n\ +type SCM_RIGHTS. Other elements of the session will be transferred as\n\ +normal data over the connection.\n\ +\n\ +We provide two classes to help applications forward socket sessions:\n\ +SocketSessionForwarder is the sender of the UNIX domain connection,\n\ +while SocketSessionReceiver is the receiver (this interface assumes\n\ +one direction of forwarding).\n\ +\n\ +Note: this paragraph and following discussions on the internal\n\ +protocol are for reference purposes only; it's not necessary to\n\ +understand how to use the API.\n\ +SocketSessionForwarder and SocketSessionReceiver objects (internally)\n\ +use a straightforward protocol to pass elements of socket sessions.\n\ +Once the connection is established, the forwarder object first forwards\n\ +the file descriptor with 1-byte dummy data. It then forwards a\n\ +\"(socket) session header\", which contains all other elements of\n\ +the session except the file descriptor (already forwarded) and session\n\ +data. The wire format of the header is as follows:\n\ +\n\ +- The length of the header (16-bit unsigned integer)\n\ +- Address family\n\ +- Socket type\n\ +- Protocol\n\ +- Size of the local endpoint in bytes\n\ +- Local endpoint (a copy of the memory image of the corresponding\n\ + sockaddr)\n\ +- Size of the remote endpoint in bytes\n\ +- Remote endpoint (same as local endpoint)\n\ +- Size of session data in bytes\n\ +\n\ +The type of the fields is 32-bit unsigned integer unless explicitly\n\ +noted, and all fields are formatted in the network byte order.\n\ +\n\ +The socket session data immediately follows the session header.\n\ +\n\ +Note that the fields do not necessarily be in the network byte order\n\ +because they are expected to be exchanged on the same machine.\n\ +Likewise, integer elements such as address family do not necessarily\n\ +be represented as an fixed-size value (i.e., 32-bit). But fixed size\n\ +fields are used in order to ensure maximum portability in such a\n\ +(rare) case where the forwarder and the receiver are built with\n\ +different compilers that have different definitions of int. Also,\n\ +since sockaddr fields are generally formatted in the network byte\n\ +order, other fields are defined so to be consistent.\n\ +\n\ +One basic assumption in the API of this module is socket sessions\n\ +should be forwarded without blocking, thus eliminating the need for\n\ +incremental read/write or blocking other important services such as\n\ +responding to requests from the application's clients. This assumption\n\ +should be held as long as both the forwarder and receiver have\n\ +sufficient resources to handle the forwarding process since the\n\ +communication is local. But a forward attempt could still block if the\n\ +receiver is busy (or even hang up) and cannot keep up with the volume\n\ +of incoming sessions.\n\ +\n\ +So, in this implementation, the forwarder uses non blocking writes to\n\ +forward sessions. If a write attempt could block, it immediately gives\n\ +up the operation with an exception. The corresponding application is\n\ +expected to catch it, close the connection, and perform any necessary\n\ +recovery for that application (that would normally be re-establish the\n\ +connection with a new receiver, possibly after confirming the\n\ +receiving side is still alive). On the other hand, the receiver\n\ +implementation assumes it's possible that it only receive incomplete\n\ +elements of a session (such as in the case where the forwarder writes\n\ +part of the entire session and gives up the connection). The receiver\n\ +implementation throws an exception when it encounters an incomplete\n\ +session. Like the case of the forwarder application, the receiver\n\ +application is expected to catch it, close the connection, and perform\n\ +any necessary recovery steps.\n\ +\n\ +Note that the receiver implementation uses blocking read. So it's\n\ +application's responsibility to ensure that there's at least some data\n\ +in the connection when the receiver object is requested to receive a\n\ +session (unless this operation can be blocking, e.g., by the use of a\n\ +separate thread). Also, if the forwarder implementation or application\n\ +is malicious or extremely buggy and intentionally sends partial\n\ +session and keeps the connection, the receiver could block in\n\ +receiving a session. In general, we assume the forwarder doesn't do\n\ +intentional blocking as it's a local node and is generally a module of\n\ +the same (BIND 10) system. The minimum requirement for the forwarder\n\ +implementation (and application) is to make sure the connection is\n\ +closed once it detects an error on it. Even a naive implementation\n\ +that simply dies due to the exception will meet this requirement.\n\ +\n\ +"; +} // unnamed namespace diff --git a/src/lib/python/isc/util/io/socketsession_python.cc b/src/lib/python/isc/util/io/socketsession_python.cc new file mode 100644 index 0000000000..7011b53d2d --- /dev/null +++ b/src/lib/python/isc/util/io/socketsession_python.cc @@ -0,0 +1,79 @@ +// Copyright (C) 2011 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 + +#include + +#include "socketsessionreceiver_python.h" +#include "socketsessionforwarder_python.h" + +using namespace isc::util::io::python; +using namespace isc::util::python; + +#include "socketsession_inc.cc" + +namespace isc { +namespace util { +namespace io { +namespace python { +PyObject* po_SocketSessionError; +} +} +} +} + +namespace { + +PyModuleDef socketsession = { + { PyObject_HEAD_INIT(NULL) NULL, 0, NULL}, + "isc.util.io.socketsession", + socketsession_doc, + -1, + NULL, + NULL, + NULL, + NULL, + NULL +}; +} // end of unnamed namespace + +PyMODINIT_FUNC +PyInit_socketsession(void) { + PyObject* mod = PyModule_Create(&socketsession); + if (mod == NULL) { + return (NULL); + } + + try { + po_SocketSessionError = + PyErr_NewException("isc.util.io.SocketSessionError", NULL, NULL); + PyObjectContainer(po_SocketSessionError). + installToModule(mod, "SocketSessionError"); + } catch (...) { + Py_DECREF(mod); + return (NULL); + } + + if (!initModulePart_SocketSessionForwarder(mod)) { + Py_DECREF(mod); + return (NULL); + } + if (!initModulePart_SocketSessionReceiver(mod)) { + Py_DECREF(mod); + return (NULL); + } + + return (mod); +} diff --git a/src/lib/python/isc/util/io/socketsession_python.h b/src/lib/python/isc/util/io/socketsession_python.h new file mode 100644 index 0000000000..b0703acb21 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsession_python.h @@ -0,0 +1,35 @@ +// Copyright (C) 2011 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 __PYTHON_SOCKETSESSION_H +#define __PYTHON_SOCKETSESSION_H 1 + +#include + +namespace isc { +namespace util { +namespace io { +namespace python { + +extern PyObject* po_SocketSessionError; + +} // namespace python +} // namespace io +} // namespace util +} // namespace isc +#endif // __PYTHON_SOCKETSESSION_H + +// Local Variables: +// mode: c++ +// End: diff --git a/src/lib/python/isc/util/io/socketsessionforwarder_inc.cc b/src/lib/python/isc/util/io/socketsessionforwarder_inc.cc new file mode 100644 index 0000000000..6b9de01155 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsessionforwarder_inc.cc @@ -0,0 +1,136 @@ +namespace { +// Modifications: +// reference to the module description (instead of "utility") +// exception description +const char* const SocketSessionForwarder_doc = "\ +The forwarder of socket sessions.\n\ +\n\ +An object of this class maintains a UNIX domain socket (normally\n\ +expected to be connected to a SocketSessionReceiver object) and\n\ +forwards socket sessions to the receiver.\n\ +\n\ +See the description of socketsession module for other details of how\n\ +the session forwarding works.\n\ +\n\ +SocketSessionForwarder(unix_file)\n\ +\n\ + The constructor.\n\ +\n\ + It's constructed with path information of the intended receiver,\n\ + but does not immediately establish a connection to the receiver;\n\ + connect_to_receiver() must be called to establish it. These are\n\ + separated so that an object of class can be initialized (possibly\n\ + as an attribute of a higher level application class object)\n\ + without knowing the receiver is ready for accepting new\n\ + forwarders. The separate connect interface allows the object to be\n\ + reused when it detects connection failure and tries to re-\n\ + establish it after closing the failed one.\n\ +\n\ + On construction, it also installs a signal filter for SIGPIPE to\n\ + ignore it. Since this class uses a stream-type connected UNIX\n\ + domain socket, if the receiver (abruptly) closes the connection a\n\ + subsequent write operation on the socket would trigger a SIGPIPE\n\ + signal, which kills the caller process by default. This behavior\n\ + would be undesirable in many cases, so this implementation always\n\ + disables the signal.\n\ +\n\ + This approach has some drawbacks, however; first, since signal\n\ + handling is process (or thread) wide, ignoring it may not what the\n\ + application wants. On the other hand, if the application changes\n\ + how the signal is handled after instantiating this class, the new\n\ + behavior affects the class operation. Secondly, even if ignoring\n\ + the signal is the desired operation, it's a waste to set the\n\ + filter every time this class object is constructed. It's\n\ + sufficient to do it once. We still adopt this behavior based on\n\ + the observation that in most cases applications would like to\n\ + ignore SIGPIPE (or simply doesn't care about it) and that this\n\ + class is not instantiated so often (so the wasteful setting\n\ + overhead should be marginal). On the other hand, doing it every\n\ + time is beneficial if the application is threaded and different\n\ + threads create different forwarder objects (and if signals work\n\ + per thread).\n\ +\n\ + Exceptions:\n\ + SocketSessionError unix_file is invalid as a path name of a UNIX\n\ + domain socket or error happens in setting a filter for\n\ + SIGPIPE (see above)\n\ + SystemError Unexpected errors such as resource allocation failure\n\ +\n\ + Parameters:\n\ + unix_file Path name of the receiver.\n\ +\n\ +"; + +// Modifications: +// exception description +const char* const SocketSessionForwarder_connectToReceiver_doc = "\ +connect_to_receiver()\n\ +\n\ +Establish a connection to the receiver.\n\ +\n\ +This method establishes a connection to the receiver at the path given\n\ +on construction. It makes the underlying UNIX domain socket non\n\ +blocking, so this method (or subsequent push() calls) does not block.\n\ +\n\ +Exceptions:\n\ + TypeError The method is called while an already established\n\ + connection is still active.\n\ + SocketSessionError A system error in socket operation.\n\ + SystemError Unexpected errors such as resource allocation failure\n\ +\n\ +"; + +// Modifications: +// bullet description +// parameters +// exception description +const char* const SocketSessionForwarder_push_doc = "\ +push(sock, family, type, protocol, local_end, remote_end, data)\n\ +\n\ +Forward a socket session to the receiver.\n\ +\n\ +This method takes a set of parameters that represent a single socket\n\ +session, renders them in the \"wire\" format according to the internal\n\ +protocol (see socketsession module) and forwards them to the\n\ +receiver through the UNIX domain connection.\n\ +\n\ +The connection must have been established by connect_to_receiver().\n\ +\n\ +For simplicity and for the convenience of detecting application\n\ +errors, this method imposes some restrictions on the parameters:\n\ +\n\ +- Socket family must be either AF_INET or AF_INET6\n\ +- The address family (sa_family) member of the local and remote end\n\ + points must be equal to the family parameter\n\ +- Socket session data must not be empty\n\ +- Data length must not exceed 65535\n\ +\n\ +These are not architectural limitation, and might be loosened in future\n\ +versions as we see the need for flexibility.\n\ +\n\ +Since the underlying UNIX domain socket is non blocking (see the\n\ +description for the constructor), a call to this method should either\n\ +return immediately or result in exception (in case of \"would\n\ +block\").\n\ +\n\ +Exceptions:\n\ + TypeError The method is called before establishing a connection or\n\ + given parameters are invalid, or the given socket address\n\ + is valid.\n\ + SocketSessionError A system error in socket operation, including the\n\ + case where the write operation would block.\n\ +\n\ +Parameters:\n\ + sock (int) The socket file descriptor\n\ + family (int) The address family (such as socket.AF_INET6) of the\n\ + socket\n\ + type (int) The socket type (such as socket.SOCK_DGRAM) of the\n\ + socket\n\ + protocol (int) The transport protocol (such as socket.IPPROTO_UDP)\n\ + of the socket\n\ + local_end (socket address) The local end point of the session\n\ + remote_end (socket address) The remote end point of the session\n\ + data (byte) the session data\n\ +\n\ +"; +} // unnamed namespace diff --git a/src/lib/python/isc/util/io/socketsessionforwarder_python.cc b/src/lib/python/isc/util/io/socketsessionforwarder_python.cc new file mode 100644 index 0000000000..ab88e065c0 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsessionforwarder_python.cc @@ -0,0 +1,305 @@ +// Copyright (C) 2011 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. + +// Enable this if you use s# variants with PyArg_ParseTuple(), see +// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers +//#define PY_SSIZE_T_CLEAN + +// Python.h needs to be placed at the head of the program file, see: +// http://docs.python.org/py3k/extending/extending.html#a-simple-example +#include + +#include +#include +#include + +#include +#include + +#include + +#include + +#include +#include +#include + +#include "socketsession_python.h" +#include "socketsessionforwarder_python.h" + +using namespace std; +using namespace isc::util::python; +using namespace isc::util::io; +using namespace isc::util::io::internal; +using namespace isc::util::io::python; +using boost::lexical_cast; + +// Trivial constructor. +s_SocketSessionForwarder::s_SocketSessionForwarder() : cppobj(NULL) { +} + +// Import pydoc text +#include "socketsessionforwarder_inc.cc" + +namespace { + +int +SocketSessionForwarder_init(PyObject* po_self, PyObject* args, PyObject*) { + s_SocketSessionForwarder* self = + static_cast(po_self); + try { + const char* unix_file; + if (PyArg_ParseTuple(args, "s", &unix_file)) { + self->cppobj = new SocketSessionForwarder(unix_file); + return (0); + } + } catch (const exception& ex) { + const string ex_what = + "Failed to construct SocketSessionForwarder object: " + + string(ex.what()); + PyErr_SetString(po_SocketSessionError, ex_what.c_str()); + return (-1); + } catch (...) { + PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception"); + return (-1); + } + + return (-1); +} + +void +SocketSessionForwarder_destroy(PyObject* po_self) { + s_SocketSessionForwarder* self = + static_cast(po_self); + delete self->cppobj; + self->cppobj = NULL; + Py_TYPE(self)->tp_free(self); +} + +// Internal exception class thrown when address parsing fails +class AddressParseError: public isc::Exception { +public: + AddressParseError(const char *file, size_t line, const char *what): + isc::Exception(file, line, what) {} +}; + +// Convert a Python socket address object to an addrinfo structure by +// getaddrinfo. +void +parsePySocketAddress(PyObject* obj, int type, int protocol, + struct sockaddr_storage* ss) +{ + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = type; + hints.ai_protocol = protocol; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + + const char* addr; + int port, flowinfo, scopeid; + struct addrinfo *res; + if (PyArg_ParseTuple(obj, "si", &addr, &port)) { + // Possibly an IPv4 address. + hints.ai_family = AF_INET; + const int error = getaddrinfo(addr, + lexical_cast(port).c_str(), + &hints, &res); + if (error == 0) { + assert(res->ai_addrlen <= sizeof(*ss)); + memcpy(ss, res->ai_addr, res->ai_addrlen); + return; + } + isc_throw(AddressParseError, "Invalid or unsupported socket address: " + << gai_strerror(error)); + } + PyErr_Clear(); + if (PyArg_ParseTuple(obj, "siii", &addr, &port, &flowinfo, &scopeid)) { + // Possibly an IPv6 address. We ignore flowinfo. + hints.ai_family = AF_INET6; + const int error = getaddrinfo(addr, + lexical_cast(port).c_str(), + &hints, &res); + if (error == 0) { + assert(res->ai_addrlen <= sizeof(*ss)); + memcpy(ss, res->ai_addr, res->ai_addrlen); + void* p = ss; + static_cast(p)->sin6_scope_id = scopeid; + return; + } + isc_throw(AddressParseError, "Invalid or unsupported socket address: " + << gai_strerror(error)); + } + PyErr_Clear(); + isc_throw(AddressParseError, "Invalid or unsupported socket address, must " + "be AF_INET or AF_INET6 socket address."); +} + +PyObject* +SocketSessionForwarder_connectToReceiver(PyObject* po_self, PyObject*) { + s_SocketSessionForwarder* const self = + static_cast(po_self); + + try { + self->cppobj->connectToReceiver(); + Py_RETURN_NONE; + } catch (const isc::BadValue& ex) { + PyErr_SetString(PyExc_TypeError, ex.what()); + return (NULL); + } catch (const SocketSessionError& ex) { + PyErr_SetString(po_SocketSessionError, ex.what()); + return (NULL); + } catch (const exception& ex) { + const string ex_what = + "Unexpected failure in connecting to receiver: " + + string(ex.what()); + PyErr_SetString(PyExc_SystemError, ex_what.c_str()); + return (NULL); + } catch (...) { + PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception"); + return (NULL); + } +} + +PyObject* +SocketSessionForwarder_push(PyObject* po_self, PyObject* args) { + s_SocketSessionForwarder* const self = + static_cast(po_self); + + try { + int fd, family, type, protocol; + PyObject* po_local_end; + PyObject* po_remote_end; + Py_buffer py_buf; + + if (!PyArg_ParseTuple(args, "iiiiOOy*", &fd, &family, &type, &protocol, + &po_local_end, &po_remote_end, &py_buf)) { + return (NULL); + } + struct sockaddr_storage ss_local, ss_remote; + parsePySocketAddress(po_local_end, type, protocol, &ss_local); + parsePySocketAddress(po_remote_end, type, protocol, &ss_remote); + self->cppobj->push(fd, family, type, protocol, + *convertSockAddr(&ss_local), + *convertSockAddr(&ss_remote), + py_buf.buf, py_buf.len); + Py_RETURN_NONE; + } catch (const AddressParseError& ex) { + PyErr_SetString(PyExc_TypeError, ex.what()); + return (NULL); + } catch (const isc::BadValue& ex) { + PyErr_SetString(PyExc_TypeError, ex.what()); + return (NULL); + } catch (const SocketSessionError& ex) { + PyErr_SetString(po_SocketSessionError, ex.what()); + return (NULL); + } catch (...) { + PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception"); + return (NULL); + } +} + +// This list contains the actual set of functions we have in +// python. Each entry has +// 1. Python method name +// 2. Our static function here +// 3. Argument type +// 4. Documentation +PyMethodDef SocketSessionForwarder_methods[] = { + { "push", SocketSessionForwarder_push, METH_VARARGS, + SocketSessionForwarder_push_doc }, + { "connect_to_receiver", SocketSessionForwarder_connectToReceiver, + METH_NOARGS, SocketSessionForwarder_connectToReceiver_doc }, + { NULL, NULL, 0, NULL } +}; +} // end of unnamed namespace + +namespace isc { +namespace util { +namespace io { +namespace python { +// This defines the complete type for reflection in python and +// parsing of PyObject* to s_SocketSessionForwarder +// Most of the functions are not actually implemented and NULL here. +PyTypeObject socketsessionforwarder_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "isc.util.io.SocketSessionForwarder", + sizeof(s_SocketSessionForwarder), // tp_basicsize + 0, // tp_itemsize + SocketSessionForwarder_destroy, // tp_dealloc + NULL, // tp_print + NULL, // tp_getattr + NULL, // tp_setattr + NULL, // tp_reserved + NULL, // tp_repr + NULL, // tp_as_number + NULL, // tp_as_sequence + NULL, // tp_as_mapping + NULL, // tp_hash + NULL, // tp_call + NULL, // tp_str + NULL, // tp_getattro + NULL, // tp_setattro + NULL, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + SocketSessionForwarder_doc, + NULL, // tp_traverse + NULL, // tp_clear + NULL, // tp_richcompare + 0, // tp_weaklistoffset + NULL, // tp_iter + NULL, // tp_iternext + SocketSessionForwarder_methods, // tp_methods + NULL, // tp_members + NULL, // tp_getset + NULL, // tp_base + NULL, // tp_dict + NULL, // tp_descr_get + NULL, // tp_descr_set + 0, // tp_dictoffset + SocketSessionForwarder_init, // tp_init + NULL, // tp_alloc + PyType_GenericNew, // tp_new + NULL, // tp_free + NULL, // tp_is_gc + NULL, // tp_bases + NULL, // tp_mro + NULL, // tp_cache + NULL, // tp_subclasses + NULL, // tp_weaklist + NULL, // tp_del + 0 // tp_version_tag +}; + +// Module Initialization, all statics are initialized here +bool +initModulePart_SocketSessionForwarder(PyObject* mod) { + // We initialize the static description object with PyType_Ready(), + // then add it to the module. This is not just a check! (leaving + // this out results in segmentation faults) + if (PyType_Ready(&socketsessionforwarder_type) < 0) { + return (false); + } + void* p = &socketsessionforwarder_type; + if (PyModule_AddObject(mod, "SocketSessionForwarder", + static_cast(p)) < 0) { + return (false); + } + Py_INCREF(&socketsessionforwarder_type); + + return (true); +} +} // namespace python +} // namespace io +} // namespace util +} // namespace isc diff --git a/src/lib/python/isc/util/io/socketsessionforwarder_python.h b/src/lib/python/isc/util/io/socketsessionforwarder_python.h new file mode 100644 index 0000000000..2ce220a7a7 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsessionforwarder_python.h @@ -0,0 +1,45 @@ +// Copyright (C) 2011 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 __PYTHON_SOCKETSESSIONFORWARDER_H +#define __PYTHON_SOCKETSESSIONFORWARDER_H 1 + +#include + +namespace isc { +namespace util { +namespace io { +class SocketSessionForwarder; + +namespace python { + +// The s_* Class simply covers one instantiation of the object +class s_SocketSessionForwarder : public PyObject { +public: + s_SocketSessionForwarder(); + SocketSessionForwarder* cppobj; +}; + +extern PyTypeObject socketsessionforwarder_type; + +bool initModulePart_SocketSessionForwarder(PyObject* mod); +} // namespace python +} // namespace io +} // namespace util +} // namespace isc +#endif // __PYTHON_SOCKETSESSIONFORWARDER_H + +// Local Variables: +// mode: c++ +// End: diff --git a/src/lib/python/isc/util/io/socketsessionreceiver_inc.cc b/src/lib/python/isc/util/io/socketsessionreceiver_inc.cc new file mode 100644 index 0000000000..ed29d3edf7 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsessionreceiver_inc.cc @@ -0,0 +1,89 @@ +namespace { +// Modifications +// - about return value +// - socket session "utility" => module +const char* const SocketSessionReceiver_doc = "\ +The receiver of socket sessions.\n\ +\n\ +An object of this class holds a UNIX domain socket for an established\n\ +connection, receives socket sessions from the remote forwarder, and\n\ +provides the session to the application as a tuple of corresponding\n\ +elements.\n\ +\n\ +Note that this class is instantiated with an already connected socket;\n\ +it's not a listening socket that is accepting connection requests from\n\ +forwarders. It's application's responsibility to create the listening\n\ +socket, listen on it and accept connections. Once the connection is\n\ +established, the application would construct a SocketSessionReceiver\n\ +object with the socket for the newly established connection. This\n\ +behavior is based on the design decision that the application should\n\ +decide when it performs (possibly) blocking operations (see\n\ +socketsession module for more details).\n\ +\n\ +See the description of socketsession module for other details of how\n\ +the session forwarding works.\n\ +\n\ +SocketSessionReceiver(socket)\n\ +\n\ + The constructor.\n\ +\n\ + Exceptions:\n\ + TypeError The given parameter is not a valid socket object\n\ + SocketSessionError Any error on an operation that is performed\n\ + on the given socket as part of initialization.\n\ + SystemError Unexpected errors such as resource allocation failure\n\ +\n\ + Parameters:\n\ + socket A python socket object of a UNIX domain family for an\n\ + established connection with a forwarder.\n\ +\n\ +"; + +// Modifications +// - socket session utility -> module +// - return value (not a SocketSession object, but a Python tuple) +// - remove the validity note (we copy it here, so there's no such +// restriction) +// - caller's responsibility: only responsible for closing the socket. +// - text around the bullets +// - exception +const char* const SocketSessionReceiver_pop_doc = "\ +pop() -> (socket, socket address, socket address, byte)\n\ +\n\ +Receive a socket session from the forwarder.\n\ +\n\ +This method receives wire-format data (see socketsession module) for\n\ +a socket session on the UNIX domain socket, performs some validation\n\ +on the data, and returns the session information as a tuple.\n\ +\n\ +The caller is responsible for closing the received socket.\n\ +\n\ +It ensures the following:\n\ +\n\ +- The socket's address family is either AF_INET or AF_INET6\n\ +- The family element of the socket addresses for the local and remote\n\ + end points must be equal to the socket's address family\n\ +- The socket session data is not empty and does not exceed 65535\n\ + bytes.\n\ +\n\ +If the validation fails or an unexpected system error happens\n\ +(including a connection close in the meddle of reception), it throws\n\ +an SocketSessionError exception. When this happens, it's very\n\ +unlikely that a subsequent call to this method succeeds, so in\n\ +reality the application is expected to destruct it and close the\n\ +socket in such a case.\n\ +\n\ +Exceptions:\n\ + SocketSessionError Invalid data is received or a system error on\n\ + socket operation happens.\n\ + SystemError Unexpected errors such as resource allocation failure\n\ +\n\ +Return Value(s): A tuple corresponding to the extracted socket session:\n\ + socket A Python socket object corresponding to the socket passed\n\ + by the forwarder\n\ + socket address A Python socket address (which is a tuple) for the local\n\ + end point\n\ + socket address A Python socket address for the remote endpoint\n\ + data A Python byte object that stores the session data\n\ +"; +} // unnamed namespace diff --git a/src/lib/python/isc/util/io/socketsessionreceiver_python.cc b/src/lib/python/isc/util/io/socketsessionreceiver_python.cc new file mode 100644 index 0000000000..29f9e13b1e --- /dev/null +++ b/src/lib/python/isc/util/io/socketsessionreceiver_python.cc @@ -0,0 +1,321 @@ +// Copyright (C) 2011 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. + +// Enable this if you use s# variants with PyArg_ParseTuple(), see +// http://docs.python.org/py3k/c-api/arg.html#strings-and-buffers +//#define PY_SSIZE_T_CLEAN + +// Python.h needs to be placed at the head of the program file, see: +// http://docs.python.org/py3k/extending/extending.html#a-simple-example +#include + +#include +#include +#include + +#include +#include + +#include + +#include + +#include + +#include "socketsession_python.h" +#include "socketsessionreceiver_python.h" + +using namespace std; +using namespace isc::util::python; +using namespace isc::util::io; +using namespace isc::util::io::python; +using boost::lexical_cast; + +// Trivial constructor. +s_SocketSessionReceiver::s_SocketSessionReceiver() : cppobj(NULL) { +} + +// Import pydoc text +#include "socketsessionreceiver_inc.cc" + +namespace { +// This C structure corresponds to a Python callable object for +// socket.fromfd(). +// See json_dumps_obj in dns_requestloader_python.cc for background rationale +// of this trick. +PyObject* socket_fromfd_obj = NULL; + +int +SocketSessionReceiver_init(PyObject* po_self, PyObject* args, PyObject*) { + s_SocketSessionReceiver* self = + static_cast(po_self); + try { + // The constructor expects a Python socket object. We'll extract + // the underlying file descriptor using the fileno method (in the + // duck typing manner) and pass it to the C++ constructor. + PyObject* po_sock; + if (PyArg_ParseTuple(args, "O", &po_sock)) { + PyObjectContainer fd_container(PyObject_CallMethod( + po_sock, + const_cast("fileno"), + NULL)); + PyObjectContainer fdarg_container( + Py_BuildValue("(O)", fd_container.get())); + int fd; + if (PyArg_ParseTuple(fdarg_container.get(), "i", &fd)) { + self->cppobj = new SocketSessionReceiver(fd); + return (0); + } + PyErr_SetString(PyExc_TypeError, "Given object's fileno() doesn't " + "return an integer, probably not a valid socket " + "object"); + } + } catch (const PyCPPWrapperException& ex) { + // This could happen due to memory allocation failure, but it's more + // likely that the object doesn't have the "fileno()" method or it + // returns an unexpected type of value. So we adjust the error + // message accordingly. + PyErr_SetString(PyExc_TypeError, "Failed to parse parameter, " + "probably not a valid socket object"); + } catch (const exception& ex) { + const string ex_what = + "Failed to construct SocketSessionReceiver object: " + + string(ex.what()); + PyErr_SetString(po_SocketSessionError, ex_what.c_str()); + } catch (...) { + PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception"); + } + + return (-1); +} + +PyObject* +createPySocketAddress(const struct sockaddr& sa) { + socklen_t salen; + if (sa.sa_family == AF_INET) { + salen = sizeof(struct sockaddr_in); + } else if (sa.sa_family == AF_INET6) { + salen = sizeof(struct sockaddr_in6); + } else { + isc_throw(SocketSessionError, "Unsupported socket address family: " + << static_cast(sa.sa_family)); + } + + char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV]; + const int error = getnameinfo(&sa, salen, hbuf, sizeof(hbuf), sbuf, + sizeof(sbuf), + NI_NUMERICHOST | NI_NUMERICSERV); + if (error != 0) { + isc_throw(SocketSessionError, "Unrecognized socket address format: " + << gai_strerror(error)); + } + if (sa.sa_family == AF_INET) { + return (Py_BuildValue("(si)", hbuf, lexical_cast(sbuf))); + } + // We know it's AF_INET6 at this point. We need some special trick for + // non-0 scope (zone) ID: getnameinfo() may convert the address to a + // textual representation using the extension described in RFC 4007, + // in which case it contains a delimiter character '%'. We need to remove + // it before constructing the tuple. The scope (zone) ID is preserved + // in the corresponding field of the tuple. + const void* p = &sa; + const struct sockaddr_in6* sin6 = + static_cast(p); + char* cp = strchr(hbuf, '%'); + if (cp != NULL) { + *cp = '\0'; + } + return (Py_BuildValue("(siii)", hbuf, lexical_cast(sbuf), 0, + sin6->sin6_scope_id)); +} + +void +SocketSessionReceiver_destroy(PyObject* po_self) { + s_SocketSessionReceiver* self = + static_cast(po_self); + delete self->cppobj; + self->cppobj = NULL; + Py_TYPE(self)->tp_free(self); +} + +// A helper struct to automatically close a socket in an RAII manner. +struct ScopedSocket : boost::noncopyable { + ScopedSocket(int fd) : fd_(fd) {} + ~ScopedSocket() { + close(fd_); + } + const int fd_; +}; + +PyObject* +SocketSessionReceiver_pop(PyObject* po_self, PyObject*) { + s_SocketSessionReceiver* const self = + static_cast(po_self); + + try { + // retrieve the session, and the convert it to a corresponding + // Python tuple. + const SocketSession session = self->cppobj->pop(); + + // We need to immediately store the socket file descriptor in a + // ScopedSocket object. socket.fromfd() will dup() the FD, so we need + // to close our copy even if an exception is thrown. + ScopedSocket sock(session.getSocket()); + + // Build Python socket object + PyObjectContainer c_args(Py_BuildValue("(iiii)", sock.fd_, + session.getFamily(), + session.getType(), + session.getProtocol())); + PyObjectContainer c_sock(PyObject_CallObject(socket_fromfd_obj, + c_args.get())); + // Convert the local and remote sockaddr to Python socket address objs + PyObjectContainer c_local(createPySocketAddress( + session.getLocalEndpoint())); + PyObjectContainer c_remote(createPySocketAddress( + session.getRemoteEndpoint())); + // Convert the session data to Python byte object. + PyObjectContainer c_data(Py_BuildValue("y#", session.getData(), + session.getDataLength())); + + // Build a tuple from them and return it. + return (Py_BuildValue("(OOOO)", c_sock.get(), c_local.get(), + c_remote.get(), c_data.get())); + } catch (const SocketSessionError& ex) { + PyErr_SetString(po_SocketSessionError, ex.what()); + return (NULL); + } catch (const exception& ex) { + const string ex_what = + "Unexpected failure in receiving a socket session: " + + string(ex.what()); + PyErr_SetString(PyExc_SystemError, ex_what.c_str()); + return (NULL); + } catch (...) { + PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception"); + return (NULL); + } +} + +// These are the functions we export + +// This list contains the actual set of functions we have in +// python. Each entry has +// 1. Python method name +// 2. Our static function here +// 3. Argument type +// 4. Documentation +PyMethodDef SocketSessionReceiver_methods[] = { + { "pop", SocketSessionReceiver_pop, METH_NOARGS, + SocketSessionReceiver_pop_doc }, + { NULL, NULL, 0, NULL } +}; +} // end of unnamed namespace + +namespace isc { +namespace util { +namespace io { +namespace python { +// This defines the complete type for reflection in python and +// parsing of PyObject* to s_SocketSessionReceiver +// Most of the functions are not actually implemented and NULL here. +PyTypeObject socketsessionreceiver_type = { + PyVarObject_HEAD_INIT(NULL, 0) + "isc.util.io.SocketSessionReceiver", + sizeof(s_SocketSessionReceiver), // tp_basicsize + 0, // tp_itemsize + SocketSessionReceiver_destroy, // tp_dealloc + NULL, // tp_print + NULL, // tp_getattr + NULL, // tp_setattr + NULL, // tp_reserved + NULL, // tp_repr + NULL, // tp_as_number + NULL, // tp_as_sequence + NULL, // tp_as_mapping + NULL, // tp_hash + NULL, // tp_call + NULL, // tp_str + NULL, // tp_getattro + NULL, // tp_setattro + NULL, // tp_as_buffer + Py_TPFLAGS_DEFAULT, // tp_flags + SocketSessionReceiver_doc, + NULL, // tp_traverse + NULL, // tp_clear + NULL, // tp_richcompare + 0, // tp_weaklistoffset + NULL, // tp_iter + NULL, // tp_iternext + SocketSessionReceiver_methods, // tp_methods + NULL, // tp_members + NULL, // tp_getset + NULL, // tp_base + NULL, // tp_dict + NULL, // tp_descr_get + NULL, // tp_descr_set + 0, // tp_dictoffset + SocketSessionReceiver_init, // tp_init + NULL, // tp_alloc + PyType_GenericNew, // tp_new + NULL, // tp_free + NULL, // tp_is_gc + NULL, // tp_bases + NULL, // tp_mro + NULL, // tp_cache + NULL, // tp_subclasses + NULL, // tp_weaklist + NULL, // tp_del + 0 // tp_version_tag +}; + +// Module Initialization, all statics are initialized here +bool +initModulePart_SocketSessionReceiver(PyObject* mod) { + // We initialize the static description object with PyType_Ready(), + // then add it to the module. This is not just a check! (leaving + // this out results in segmentation faults) + if (PyType_Ready(&socketsessionreceiver_type) < 0) { + return (false); + } + void* p = &socketsessionreceiver_type; + if (PyModule_AddObject(mod, "SocketSessionReceiver", + static_cast(p)) < 0) { + return (false); + } + + PyObject* socket_module = PyImport_AddModule("socket"); + if (socket_module != NULL) { + PyObject* socket_dict = PyModule_GetDict(socket_module); + if (socket_dict != NULL) { + socket_fromfd_obj = PyDict_GetItemString(socket_dict, "fromfd"); + } + } + if (socket_fromfd_obj != NULL) { + Py_INCREF(socket_fromfd_obj); + } else { + PyErr_SetString(PyExc_RuntimeError, + "isc.util.io.SocketSessionReceiver needs " + "socket.fromfd(), but it's missing"); + return (false); + } + + Py_INCREF(&socketsessionreceiver_type); + + return (true); +} + +} // namespace python +} // namespace io +} // namespace util +} // namespace isc diff --git a/src/lib/python/isc/util/io/socketsessionreceiver_python.h b/src/lib/python/isc/util/io/socketsessionreceiver_python.h new file mode 100644 index 0000000000..14e8a1b512 --- /dev/null +++ b/src/lib/python/isc/util/io/socketsessionreceiver_python.h @@ -0,0 +1,46 @@ +// Copyright (C) 2011 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 __PYTHON_SOCKETSESSIONRECEIVER_H +#define __PYTHON_SOCKETSESSIONRECEIVER_H 1 + +#include + +namespace isc { +namespace util { +namespace io { +class SocketSessionReceiver; + +namespace python { + +// The s_* Class simply covers one instantiation of the object +class s_SocketSessionReceiver : public PyObject { +public: + s_SocketSessionReceiver(); + SocketSessionReceiver* cppobj; +}; + +extern PyTypeObject socketsessionreceiver_type; + +bool initModulePart_SocketSessionReceiver(PyObject* mod); + +} // namespace io +} // namespace python +} // namespace util +} // namespace isc +#endif // __PYTHON_SOCKETSESSIONRECEIVER_H + +// Local Variables: +// mode: c++ +// End: diff --git a/src/lib/python/isc/util/io/tests/Makefile.am b/src/lib/python/isc/util/io/tests/Makefile.am new file mode 100644 index 0000000000..4adec3a838 --- /dev/null +++ b/src/lib/python/isc/util/io/tests/Makefile.am @@ -0,0 +1,34 @@ +PYCOVERAGE_RUN = @PYCOVERAGE_RUN@ +PYTESTS = socketsession_test.py +EXTRA_DIST = $(PYTESTS) + +# If necessary (rare cases), explicitly specify paths to dynamic libraries +# required by loadable python modules. +LIBRARY_PATH_PLACEHOLDER = +if SET_ENV_LIBRARY_PATH +PATHS1 = $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs$$$(ENV_LIBRARY_PATH) +PATHS2 = $(abs_top_builddir)/src/lib/dns/.libs +LIBRARY_PATH_PLACEHOLDER += $(PATHS1):$(PATHS2) +endif + +# test using command-line arguments, so use check-local target instead of TESTS +check-local: +if ENABLE_PYTHON_COVERAGE + touch $(abs_top_srcdir)/.coverage + rm -f .coverage + ${LN_S} $(abs_top_srcdir)/.coverage .coverage +endif + for pytest in $(PYTESTS) ; do \ + echo Running test: $$pytest ; \ + PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/isc/python/util/io/.libs \ + $(LIBRARY_PATH_PLACEHOLDER) \ + TESTDATAOBJDIR=$(abs_builddir) \ + $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \ + done + +CLEANFILES = *.unix + +CLEANDIRS = __pycache__ + +clean-local: + rm -rf $(CLEANDIRS) diff --git a/src/lib/python/isc/util/io/tests/socketsession_test.py b/src/lib/python/isc/util/io/tests/socketsession_test.py new file mode 100644 index 0000000000..a2c95a041e --- /dev/null +++ b/src/lib/python/isc/util/io/tests/socketsession_test.py @@ -0,0 +1,253 @@ +# Copyright (C) 2011 Internet Systems Consortium. +# +# Permission to use, copy, modify, and 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 INTERNET SYSTEMS CONSORTIUM +# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL +# INTERNET SYSTEMS CONSORTIUM 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. + +import os, signal, socket, unittest +from socket import AF_INET, AF_INET6, SOCK_STREAM, SOCK_DGRAM, IPPROTO_UDP, \ + IPPROTO_TCP +from isc.util.io.socketsession import * + +TESTDATA_OBJDIR = os.getenv("TESTDATAOBJDIR") +TEST_UNIX_FILE = TESTDATA_OBJDIR + '/test.unix' +TEST_DATA = b'BIND10 test' +TEST_PORT = 53535 + +class TestForwarder(unittest.TestCase): + '''In general, this is a straightforward port of the C++ counterpart. + + In some cases test cases are simplified or have Python specific cases. + + ''' + + def setUp(self): + self.forwarder = SocketSessionForwarder(TEST_UNIX_FILE) + if os.path.exists(TEST_UNIX_FILE): + os.unlink(TEST_UNIX_FILE) + self.large_text = b'a' * 65535 + + def tearDown(self): + if os.path.exists(TEST_UNIX_FILE): + os.unlink(TEST_UNIX_FILE) + + def start_listen(self): + self.listen_sock = socket.socket(socket.AF_UNIX, SOCK_STREAM, 0) + self.listen_sock.bind(TEST_UNIX_FILE) + self.listen_sock.listen(10) + + def accept_forwarder(self): + self.listen_sock.setblocking(False) + s, _ = self.listen_sock.accept() + s.setblocking(True) + return s + + def test_init(self): + # check bad arguments. valid cases will covered in other tests. + self.assertRaises(TypeError, SocketSessionForwarder, 1) + self.assertRaises(TypeError, SocketSessionForwarder, + 'test.unix', 'test.unix') + + def test_badpush(self): + # bad numbers of parameters + self.assertRaises(TypeError, self.forwarder.push, 1) + self.assertRaises(TypeError, self.forwarder.push, 0, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('127.0.0.1', 53), + ('192.0.2.1', 5300), TEST_DATA, 0) + # contain a bad type of parameter + self.assertRaises(TypeError, self.forwarder.push, 0, 'AF_INET', + SOCK_DGRAM, IPPROTO_UDP, ('127.0.0.1', 53), + ('192.0.2.1', 5300), TEST_DATA) + # bad local address + self.assertRaises(TypeError, self.forwarder.push, 0, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('127.0.0..1', 53), + ('192.0.2.1', 5300), TEST_DATA) + self.assertRaises(TypeError, self.forwarder.push, 0, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, '127.0.0.1', + ('192.0.2.1', 5300), TEST_DATA) + # bad remote address + self.assertRaises(TypeError, self.forwarder.push, 0, AF_INET6, + SOCK_DGRAM, IPPROTO_UDP, ('2001:db8::1', 53), + ('2001:db8:::3', 5300), TEST_DATA) + + # push before connect + self.assertRaises(TypeError, self.forwarder.push, 0, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('192.0.2.1', 53), + ('192.0.2.2', 53), TEST_DATA) + + # Now connect the forwarder for the rest of tests + self.start_listen() + self.forwarder.connect_to_receiver() + + # Inconsistent address family + self.assertRaises(TypeError, self.forwarder.push, 1, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('2001:db8::1', 53, 0, 1), + ('192.0.2.2', 53), TEST_DATA) + self.assertRaises(TypeError, self.forwarder.push, 1, AF_INET6, + SOCK_DGRAM, IPPROTO_UDP, ('2001:db8::1', 53, 0, 1), + ('192.0.2.2', 53), TEST_DATA) + + # Empty data: we reject them at least for now + self.assertRaises(TypeError, self.forwarder.push, 1, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('192.0.2.1', 53), + ('192.0.2.2', 53), b'') + + # Too big data: we reject them at least for now + self.assertRaises(TypeError, self.forwarder.push, 1, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('192.0.2.1', 53), + ('192.0.2.2', 53), b'd' * 65536) + + # Close the receptor before push. It will result in SIGPIPE (should be + # ignored) and EPIPE, which will be converted to SocketSessionError. + self.listen_sock.close() + self.assertRaises(SocketSessionError, self.forwarder.push, 1, AF_INET, + SOCK_DGRAM, IPPROTO_UDP, ('192.0.2.1', 53), + ('192.0.2.2', 53), TEST_DATA) + + def create_socket(self, family, type, protocol, addr, do_listen): + s = socket.socket(family, type, protocol) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(addr) + if do_listen and protocol == IPPROTO_TCP: + s.listen(1) + return s + + def check_push_and_pop(self, family, type, protocol, local, remote, + data, new_connection): + sock = self.create_socket(family, type, protocol, local, True) + fwd_fd = sock.fileno() + if protocol == IPPROTO_TCP: + client_addr = ('::1', 0, 0, 0) if family == AF_INET6 \ + else ('127.0.0.1', 0) + client_sock = self.create_socket(family, type, protocol, + client_addr, False) + client_sock.setblocking(False) + try: + client_sock.connect(local) + except socket.error: + pass + server_sock, _ = sock.accept() + fwd_fd = server_sock.fileno() + + # If a new connection is required, start the "server", have the + # internal forwarder connect to it, and then internally accept it. + if new_connection: + self.start_listen() + self.forwarder.connect_to_receiver() + self.accept_sock = self.accept_forwarder() + + # Then push one socket session via the forwarder. + self.forwarder.push(fwd_fd, family, type, protocol, local, remote, + data) + + # Pop the socket session we just pushed from a local receiver, and + # check the content. + receiver = SocketSessionReceiver(self.accept_sock) + signal.alarm(1) + sock_session = receiver.pop() + signal.alarm(0) + passed_sock = sock_session[0] + self.assertNotEqual(fwd_fd, passed_sock.fileno()) + self.assertEqual(family, passed_sock.family) + self.assertEqual(type, passed_sock.type) + self.assertEqual(protocol, passed_sock.proto) + self.assertEqual(local, sock_session[1]) + self.assertEqual(remote, sock_session[2]) + self.assertEqual(data, sock_session[3]) + + # Check if the passed FD is usable by sending some data from it. + passed_sock.setblocking(True) + if protocol == IPPROTO_UDP: + self.assertEqual(len(TEST_DATA), passed_sock.sendto(TEST_DATA, + local)) + sock.settimeout(10) + self.assertEqual(TEST_DATA, sock.recvfrom(len(TEST_DATA))[0]) + else: + server_sock.close() + self.assertEqual(len(TEST_DATA), passed_sock.send(TEST_DATA)) + client_sock.setblocking(True) + client_sock.settimeout(10) + self.assertEqual(TEST_DATA, client_sock.recv(len(TEST_DATA))) + + def test_push_and_pop(self): + # This is a straightforward port of C++ pushAndPop test. + local6 = ('::1', TEST_PORT, 0, 0) + remote6 = ('2001:db8::1', 5300, 0, 0) + self.check_push_and_pop(AF_INET6, SOCK_DGRAM, IPPROTO_UDP, + local6, remote6, TEST_DATA, True) + self.check_push_and_pop(AF_INET6, SOCK_STREAM, IPPROTO_TCP, + local6, remote6, TEST_DATA, False) + + local4 = ('127.0.0.1', TEST_PORT) + remote4 = ('192.0.2.2', 5300) + self.check_push_and_pop(AF_INET, SOCK_DGRAM, IPPROTO_UDP, + local4, remote4, TEST_DATA, False) + self.check_push_and_pop(AF_INET, SOCK_STREAM, IPPROTO_TCP, + local4, remote4, TEST_DATA, False) + + self.check_push_and_pop(AF_INET6, SOCK_DGRAM, IPPROTO_UDP, + local6, remote6, self.large_text, False) + self.check_push_and_pop(AF_INET6, SOCK_STREAM, IPPROTO_TCP, + local6, remote6, self.large_text, False) + self.check_push_and_pop(AF_INET, SOCK_DGRAM, IPPROTO_UDP, + local4, remote4, self.large_text, False) + self.check_push_and_pop(AF_INET, SOCK_STREAM, IPPROTO_TCP, + local4, remote4, self.large_text, False) + + # Python specific: check for an IPv6 scoped address with non 0 + # scope (zone) ID + scope6 = ('fe80::1', TEST_PORT, 0, 1) + self.check_push_and_pop(AF_INET6, SOCK_DGRAM, IPPROTO_UDP, + local6, scope6, TEST_DATA, False) + + def test_push_too_fast(self): + # A straightforward port of C++ pushTooFast test. + def multi_push(forwarder, addr, data): + for i in range(0, 10): + forwarder.push(1, AF_INET, SOCK_DGRAM, IPPROTO_UDP, addr, + addr, data) + self.start_listen() + self.forwarder.connect_to_receiver() + self.assertRaises(SocketSessionError, multi_push, self.forwarder, + ('192.0.2.1', 53), self.large_text) + + def test_bad_pop(self): + # This is a subset of C++ badPop test. We only check pop() raises + # SocketSessionError when it internally fails to get the FD. + # Other cases would require passing a valid FD from the test, + # which would make the test too complicated. As a wrapper checking + # one common failure case should be reasonably sufficient. + + self.start_listen() + s = socket.socket(socket.AF_UNIX, SOCK_STREAM, 0) + s.setblocking(False) + s.connect(TEST_UNIX_FILE) + accept_sock = self.accept_forwarder() + receiver = SocketSessionReceiver(accept_sock) + s.close() + self.assertRaises(SocketSessionError, receiver.pop) + +class TestReceiver(unittest.TestCase): + # We only check a couple of failure cases on construction. Valid cases + # are covered in TestForwarder. + + def test_bad_init(self): + class FakeSocket: + # pretending to be th standard socket class, but its fileno() is + # bogus. + def fileno(self): + return None + self.assertRaises(TypeError, SocketSessionReceiver, 1) + self.assertRaises(TypeError, SocketSessionReceiver, FakeSocket()) + +if __name__ == '__main__': + unittest.main()