Commit ae1cf18d authored by JINMEI Tatuya's avatar JINMEI Tatuya
Browse files

[trac1104] allowed python binding to have TSIG in RequestContext.

parent 26e04c45
......@@ -4,10 +4,10 @@ AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
AM_CXXFLAGS = $(B10_CXXFLAGS)
python_PYTHON = __init__.py
python_PYTHON = __init__.py dns.py
pythondir = $(PYTHON_SITEPKG_DIR)/isc/acl
pyexec_LTLIBRARIES = acl.la dns.la
pyexec_LTLIBRARIES = acl.la _dns.la
pyexecdir = $(PYTHON_SITEPKG_DIR)/isc/acl
acl_la_SOURCES = acl.cc
......@@ -15,14 +15,14 @@ acl_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
acl_la_LDFLAGS = $(PYTHON_LDFLAGS)
acl_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)
dns_la_SOURCES = dns.h dns.cc dns_requestacl_python.h dns_requestacl_python.cc
dns_la_SOURCES += dns_requestcontext_python.h dns_requestcontext_python.cc
dns_la_SOURCES += dns_requestloader_python.h dns_requestloader_python.cc
dns_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
dns_la_LDFLAGS = $(PYTHON_LDFLAGS)
_dns_la_SOURCES = dns.h dns.cc dns_requestacl_python.h dns_requestacl_python.cc
_dns_la_SOURCES += dns_requestcontext_python.h dns_requestcontext_python.cc
_dns_la_SOURCES += dns_requestloader_python.h dns_requestloader_python.cc
_dns_la_CPPFLAGS = $(AM_CPPFLAGS) $(PYTHON_INCLUDES)
_dns_la_LDFLAGS = $(PYTHON_LDFLAGS)
# Note: PYTHON_CXXFLAGS may have some -Wno... workaround, which must be
# placed after -Wextra defined in AM_CXXFLAGS
dns_la_CXXFLAGS = $(AM_CXXFLAGS) $(PYTHON_CXXFLAGS)
_dns_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.
......@@ -30,9 +30,9 @@ acl_la_LDFLAGS += -module
acl_la_LIBADD = $(top_builddir)/src/lib/acl/libacl.la
acl_la_LIBADD += $(PYTHON_LIB)
dns_la_LDFLAGS += -module
dns_la_LIBADD = $(top_builddir)/src/lib/acl/libdnsacl.la
dns_la_LIBADD += $(PYTHON_LIB)
_dns_la_LDFLAGS += -module
_dns_la_LIBADD = $(top_builddir)/src/lib/acl/libdnsacl.la
_dns_la_LIBADD += $(PYTHON_LIB)
EXTRA_DIST = acl.py dns.py
EXTRA_DIST += acl_inc.cc
......
......@@ -52,7 +52,7 @@ PyMethodDef methods[] = {
PyModuleDef dnsacl = {
{ PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
"isc.acl.dns",
"isc.acl._dns",
dnsacl_doc,
-1,
methods,
......@@ -90,7 +90,7 @@ getACLException(const char* ex_name) {
}
PyMODINIT_FUNC
PyInit_dns(void) {
PyInit__dns(void) {
PyObject* mod = PyModule_Create(&dnsacl);
if (mod == NULL) {
return (NULL);
......
......@@ -13,21 +13,61 @@
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# This file is not installed. The log.so is installed into the right place.
# It is only to find it in the .libs directory when we run as a test or
# from the build directory.
# But as nobody gives us the builddir explicitly (and we can't use generation
# from .in file, as it would put us into the builddir and we wouldn't be found)
# we guess from current directory. Any idea for something better? This should
# be enough for the tests, but would it work for B10_FROM_SOURCE as well?
# Should we look there? Or define something in bind10_config?
import os
import sys
for base in sys.path[:]:
bindingdir = os.path.join(base, 'isc/acl/.libs')
if os.path.exists(bindingdir):
sys.path.insert(0, bindingdir)
from dns import *
"""\
This module provides Python bindings for the C++ classes in the
isc::acl::dns namespace. Specifically, it defines Python interfaces of
handling access control lists (ACLs) with DNS related contexts.
The actual binding is implemented in an effectively hidden module,
isc.acl._dns; this frontend module is in terms of implementation so that
the C++ binding code doesn't have to deal with complicated operations
that could be done in a more straightforward way in native Python.
For further details of the actual module, see the documentation of the
_dns module.
"""
import pydnspp
import isc.acl._dns
from isc.acl._dns import *
class RequestACL(isc.acl._dns.RequestACL):
"""A straightforward wrapper subclass of isc.acl._dns.RequestACL.
See the base class documentation for more implementation.
"""
pass
class RequestLoader(isc.acl._dns.RequestLoader):
"""A straightforward wrapper subclass of isc.acl._dns.RequestLoader.
See the base class documentation for more implementation.
"""
pass
class RequestContext(isc.acl._dns.RequestContext):
"""A straightforward wrapper subclass of isc.acl._dns.RequestContext.
See the base class documentation for more implementation.
"""
def __init__(self, remote_address, tsig=None):
"""Wrapper for the RequestContext constructor.
Internal implementation details that the users don't have to
worry about: To avoid dealing with pydnspp bindings in the C++ code,
this wrapper converts the TSIG record in its wire format in the form
of byte data, and has the binding re-construct the record from it.
"""
tsig_wire = b''
if tsig is not None:
if not isinstance(tsig, pydnspp.TSIGRecord):
raise TypeError("tsig must be a TSIGRecord, not %s" %
tsig.__class__.__name__)
tsig_wire = tsig.to_wire(tsig_wire)
isc.acl._dns.RequestContext.__init__(self, remote_address, tsig_wire)
def __str__(self):
"""Wrap __str__() to convert the module name."""
s = isc.acl._dns.RequestContext.__str__(self)
return s.replace('<isc.acl._dns', '<isc.acl.dns')
......@@ -114,7 +114,7 @@ namespace python {
// Most of the functions are not actually implemented and NULL here.
PyTypeObject requestacl_type = {
PyVarObject_HEAD_INIT(NULL, 0)
"isc.acl.dns.RequestACL",
"isc.acl._dns.RequestACL",
sizeof(s_RequestACL), // tp_basicsize
0, // tp_itemsize
RequestACL_destroy, // tp_dealloc
......@@ -132,7 +132,7 @@ PyTypeObject requestacl_type = {
NULL, // tp_getattro
NULL, // tp_setattro
NULL, // tp_as_buffer
Py_TPFLAGS_DEFAULT, // tp_flags
Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, // tp_flags
RequestACL_doc,
NULL, // tp_traverse
NULL, // tp_clear
......
......@@ -14,7 +14,7 @@
// 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
#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
......@@ -37,8 +37,16 @@
#include <exceptions/exceptions.h>
#include <util/buffer.h>
#include <util/python/pycppwrapper_util.h>
#include <dns/name.h>
#include <dns/rrclass.h>
#include <dns/rrtype.h>
#include <dns/rrttl.h>
#include <dns/rdata.h>
#include <dns/tsigrecord.h>
#include <acl/dns.h>
#include <acl/ip_check.h>
......@@ -49,6 +57,8 @@ using namespace std;
using boost::scoped_ptr;
using boost::lexical_cast;
using namespace isc;
using namespace isc::dns;
using namespace isc::dns::rdata;
using namespace isc::util::python;
using namespace isc::acl::dns;
using namespace isc::acl::dns::python;
......@@ -59,11 +69,39 @@ namespace dns {
namespace python {
struct s_RequestContext::Data {
// The constructor. Currently it only accepts the information of the
// request source address, and contains all necessary logic in the body
// of the constructor. As it's extended we may have refactor it by
// introducing helper methods.
Data(const char* const remote_addr, const unsigned short remote_port) {
// The constructor.
Data(const char* const remote_addr, const unsigned short remote_port,
const char* tsig_data, const Py_ssize_t tsig_len)
{
createRemoteAddr(remote_addr, remote_port);
createTSIGRecord(tsig_data, tsig_len);
}
// A convenient type converter from sockaddr_storage to sockaddr
const struct sockaddr& getRemoteSockaddr() const {
const void* p = &remote_ss;
return (*static_cast<const struct sockaddr*>(p));
}
// The remote (source) IP address of the request. Note that it needs
// a reference to remote_ss. That's why the latter is stored within
// this structure.
scoped_ptr<IPAddress> remote_ipaddr;
// The effective length of remote_ss. It's necessary for getnameinfo()
// called from sockaddrToText (__str__ backend).
socklen_t remote_salen;
// The TSIG record included in the request, if any. If the request
// doesn't contain a TSIG, this will be NULL.
scoped_ptr<TSIGRecord> tsig_record;
private:
// A helper method for the constructor that is responsible for constructing
// the remote address.
void createRemoteAddr(const char* const remote_addr,
const unsigned short remote_port)
{
struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
......@@ -85,20 +123,31 @@ struct s_RequestContext::Data {
remote_ipaddr.reset(new IPAddress(getRemoteSockaddr()));
}
// A convenient type converter from sockaddr_storage to sockaddr
const struct sockaddr& getRemoteSockaddr() const {
const void* p = &remote_ss;
return (*static_cast<const struct sockaddr*>(p));
}
// The remote (source) IP address the request. Note that it needs
// a reference to remote_ss. That's why the latter is stored within
// this structure.
scoped_ptr<IPAddress> remote_ipaddr;
// A helper method for the constructor that is responsible for constructing
// the request TSIG.
void createTSIGRecord(const char* tsig_data, const Py_ssize_t tsig_len) {
if (tsig_len == 0) {
return;
}
// The effective length of remote_ss. It's necessary for getnameinf()
// called from sockaddrToText (__str__ backend).
socklen_t remote_salen;
// Re-construct the TSIG record from the passed binary. This should
// normally succeed because we are generally expected to be called
// from the frontend .py, which converts a valid TSIGRecord in its
// wire format. If some evil or buggy python program directly calls
// us with bogus data, validation in libdns++ will trigger an
// exception, which will be caught and converted to a Python exception
// RequestContext_init().
isc::util::InputBuffer b(tsig_data, tsig_len);
const Name key_name(b);
const RRType tsig_type(b.readUint16());
const RRClass tsig_class(b.readUint16());
const RRTTL ttl(b.readUint32());
const size_t rdlen(b.readUint16());
const ConstRdataPtr rdata = createRdata(tsig_type, tsig_class, b,
rdlen);
tsig_record.reset(new TSIGRecord(key_name, tsig_class, ttl,
*rdata, 0));
}
private:
struct sockaddr_storage remote_ss;
......@@ -145,31 +194,41 @@ RequestContext_init(PyObject* po_self, PyObject* args, PyObject*) {
s_RequestContext* const self = static_cast<s_RequestContext*>(po_self);
try {
// In this initial implementation, the constructor is simply: It
// takes a single parameter, which should be a Python socket address
// object. For IPv4, it's ('address test', numeric_port); for IPv6,
// In this initial implementation, the constructor is simple: It
// takes two parameters. The first parameter should be a Python
// socket address object.
// For IPv4, it's ('address test', numeric_port); for IPv6,
// it's ('address text', num_port, num_flowid, num_zoneid).
// The second parameter is wire-format TSIG record in the form of
// Python byte data. If the TSIG isn't included in the request,
// its length will be 0.
// Below, we parse the argument in the most straightforward way.
// As the constructor becomes more complicated, we should probably
// make it more structural (for example, we should first retrieve
// the socket address as a PyObject, and parse it recursively)
// the python objects, and parse them recursively)
const char* remote_addr;
unsigned short remote_port;
unsigned int remote_flowinfo; // IPv6 only, unused here
unsigned int remote_zoneid; // IPv6 only, unused here
if (PyArg_ParseTuple(args, "(sH)", &remote_addr, &remote_port) ||
PyArg_ParseTuple(args, "(sHII)", &remote_addr, &remote_port,
&remote_flowinfo, &remote_zoneid))
const char* tsig_data;
Py_ssize_t tsig_len;
if (PyArg_ParseTuple(args, "(sH)y#", &remote_addr, &remote_port,
&tsig_data, &tsig_len) ||
PyArg_ParseTuple(args, "(sHII)y#", &remote_addr, &remote_port,
&remote_flowinfo, &remote_zoneid,
&tsig_data, &tsig_len))
{
// We need to clear the error in case the first call to PareTuple
// fails.
PyErr_Clear();
auto_ptr<s_RequestContext::Data> dataptr(
new s_RequestContext::Data(remote_addr, remote_port));
self->cppobj = new RequestContext(*dataptr->remote_ipaddr);
new s_RequestContext::Data(remote_addr, remote_port,
tsig_data, tsig_len));
self->cppobj = new RequestContext(*dataptr->remote_ipaddr,
dataptr->tsig_record.get());
self->data_ = dataptr.release();
return (0);
}
......@@ -224,7 +283,11 @@ RequestContext_str(PyObject* po_self) {
objss << "<" << requestcontext_type.tp_name << " object, "
<< "remote_addr="
<< sockaddrToText(self->data_->getRemoteSockaddr(),
self->data_->remote_salen) << ">";
self->data_->remote_salen);
if (self->data_->tsig_record) {
objss << ", key=" << self->data_->tsig_record->getName();
}
objss << ">";
return (Py_BuildValue("s", objss.str().c_str()));
} catch (const exception& ex) {
const string ex_what =
......@@ -248,7 +311,7 @@ namespace python {
// Most of the functions are not actually implemented and NULL here.
PyTypeObject requestcontext_type = {
PyVarObject_HEAD_INIT(NULL, 0)
"isc.acl.dns.RequestContext",
"isc.acl._dns.RequestContext",
sizeof(s_RequestContext), // tp_basicsize
0, // tp_itemsize
RequestContext_destroy, // tp_dealloc
......@@ -266,7 +329,7 @@ PyTypeObject requestcontext_type = {
NULL, // tp_getattro
NULL, // tp_setattro
NULL, // tp_as_buffer
Py_TPFLAGS_DEFAULT, // tp_flags
Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, // tp_flags
RequestContext_doc,
NULL, // tp_traverse
NULL, // tp_clear
......
......@@ -171,7 +171,7 @@ namespace python {
// Most of the functions are not actually implemented and NULL here.
PyTypeObject requestloader_type = {
PyVarObject_HEAD_INIT(NULL, 0)
"isc.acl.dns.RequestLoader",
"isc.acl._dns.RequestLoader",
sizeof(s_RequestLoader), // tp_basicsize
0, // tp_itemsize
RequestLoader_destroy, // tp_dealloc
......@@ -189,7 +189,7 @@ PyTypeObject requestloader_type = {
NULL, // tp_getattro
NULL, // tp_setattro
NULL, // tp_as_buffer
Py_TPFLAGS_DEFAULT, // tp_flags
Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, // tp_flags
RequestLoader_doc,
NULL, // tp_traverse
NULL, // tp_clear
......
......@@ -19,7 +19,7 @@ if ENABLE_PYTHON_COVERAGE
endif
for pytest in $(PYTESTS) ; do \
echo Running test: $$pytest ; \
env PYTHONPATH=$(abs_top_builddir)/src/lib/isc/python/acl/.libs:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python \
env PYTHONPATH=$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/isc/python/acl/.libs:$(abs_top_srcdir)/src/lib/python:$(abs_top_builddir)/src/lib/python \
$(LIBRARY_PATH_PLACEHOLDER) \
$(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
done
......
......@@ -15,6 +15,7 @@
import unittest
import socket
from pydnspp import *
from isc.acl.acl import LoaderError, Error, ACCEPT, REJECT, DROP
from isc.acl.dns import *
......@@ -39,12 +40,37 @@ def get_acl_json(prefix):
json[0]["from"] = prefix
return REQUEST_LOADER.load(json)
def get_context(address):
# The following two are similar to the previous two, but use a TSIG key name
# instead of IP prefix.
def get_tsig_acl(key):
return REQUEST_LOADER.load('[{"action": "ACCEPT", "key": "' + \
key + '"}]')
def get_tsig_acl_json(key):
json = [{"action": "ACCEPT"}]
json[0]["key"] = key
return REQUEST_LOADER.load(json)
# commonly used TSIG RDATA. For the purpose of ACL checks only the key name
# matters; other parrameters are simply borrowed from some other tests, which
# can be anything for the purpose of the tests here.
TSIG_RDATA = TSIG("hmac-md5.sig-alg.reg.int. 1302890362 " + \
"300 16 2tra2tra2tra2tra2tra2g== " + \
"11621 0 0")
def get_context(address, key_name=None):
'''This is a simple shortcut wrapper for creating a RequestContext
object with a given IP address. Port number doesn't matter in the test
(as of the initial implementation), so it's fixed for simplicity.
object with a given IP address and optionally TSIG key name.
Port number doesn't matter in the test (as of the initial implementation),
so it's fixed for simplicity.
If key_name is not None, it internally creates a (faked) TSIG record
and constructs a context with that key. Note that only the key name
matters for the purpose of ACL checks.
'''
return RequestContext(get_sockaddr(address, 53000))
tsig_record = None
if key_name is not None:
tsig_record = TSIGRecord(Name(key_name), TSIG_RDATA)
return RequestContext(get_sockaddr(address, 53000), tsig_record)
# These are commonly used RequestContext object
CONTEXT4 = get_context('192.0.2.1')
......@@ -63,6 +89,21 @@ class RequestContextTest(unittest.TestCase):
RequestContext(('2001:db8::1234', 53006,
0, 0)).__str__())
# Construct the context from IP address and a TSIG record.
tsig_record = TSIGRecord(Name("key.example.com"), TSIG_RDATA)
self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
'remote_addr=[192.0.2.1]:53001, ' + \
'key=key.example.com.>',
RequestContext(('192.0.2.1', 53001),
tsig_record).__str__())
# same with IPv6 address, just in case.
self.assertEqual('<isc.acl.dns.RequestContext object, ' + \
'remote_addr=[2001:db8::1234]:53006, ' + \
'key=key.example.com.>',
RequestContext(('2001:db8::1234', 53006,
0, 0), tsig_record).__str__())
# Unusual case: port number overflows (this constructor allows that,
# although it should be rare anyway; the socket address should
# normally come from the Python socket module.
......@@ -89,7 +130,9 @@ class RequestContextTest(unittest.TestCase):
# not a tuple
self.assertRaises(TypeError, RequestContext, 1)
# invalid number of parameters
self.assertRaises(TypeError, RequestContext, ('192.0.2.1', 53), 0)
self.assertRaises(TypeError, RequestContext, ('192.0.2.1', 53), 0, 1)
# type error for TSIG
self.assertRaises(TypeError, RequestContext, ('192.0.2.1', 53), tsig=1)
# tuple is not in the form of sockaddr
self.assertRaises(TypeError, RequestContext, (0, 53))
self.assertRaises(TypeError, RequestContext, ('192.0.2.1', 'http'))
......@@ -158,10 +201,22 @@ class RequestACLTest(unittest.TestCase):
'[{"action": "ACCEPT", "from": []}]')
self.assertRaises(LoaderError, REQUEST_LOADER.load,
[{"action": "ACCEPT", "from": []}])
self.assertRaises(LoaderError, REQUEST_LOADER.load,
'[{"action": "ACCEPT", "key": 1}]')
self.assertRaises(LoaderError, REQUEST_LOADER.load,
[{"action": "ACCEPT", "key": 1}])
self.assertRaises(LoaderError, REQUEST_LOADER.load,
'[{"action": "ACCEPT", "key": {}}]')
self.assertRaises(LoaderError, REQUEST_LOADER.load,
[{"action": "ACCEPT", "key": {}}])
self.assertRaises(LoaderError, REQUEST_LOADER.load,
'[{"action": "ACCEPT", "from": "bad"}]')
self.assertRaises(LoaderError, REQUEST_LOADER.load,
[{"action": "ACCEPT", "from": "bad"}])
self.assertRaises(LoaderError, REQUEST_LOADER.load,
[{"action": "ACCEPT", "key": "bad..name"}])
self.assertRaises(LoaderError, REQUEST_LOADER.load,
[{"action": "ACCEPT", "key": "bad..name"}])
self.assertRaises(LoaderError, REQUEST_LOADER.load,
'[{"action": "ACCEPT", "from": null}]')
self.assertRaises(LoaderError, REQUEST_LOADER.load,
......@@ -237,6 +292,28 @@ class RequestACLTest(unittest.TestCase):
self.assertEqual(REJECT, get_acl('32.1.13.184').execute(CONTEXT6))
self.assertEqual(REJECT, get_acl_json('32.1.13.184').execute(CONTEXT6))
# TSIG checks, derived from dns_test.cc
self.assertEqual(ACCEPT, get_tsig_acl('key.example.com').\
execute(get_context('192.0.2.1',
'key.example.com')))
self.assertEqual(REJECT, get_tsig_acl_json('key.example.com').\
execute(get_context('192.0.2.1',
'badkey.example.com')))
self.assertEqual(ACCEPT, get_tsig_acl('key.example.com').\
execute(get_context('2001:db8::1',
'key.example.com')))
self.assertEqual(REJECT, get_tsig_acl_json('key.example.com').\
execute(get_context('2001:db8::1',
'badkey.example.com')))
self.assertEqual(REJECT, get_tsig_acl('key.example.com').\
execute(CONTEXT4))
self.assertEqual(REJECT, get_tsig_acl_json('key.example.com').\
execute(CONTEXT4))
self.assertEqual(REJECT, get_tsig_acl('key.example.com').\
execute(CONTEXT6))
self.assertEqual(REJECT, get_tsig_acl_json('key.example.com').\
execute(CONTEXT6))
# A bit more complicated example, derived from resolver_config_unittest
acl = REQUEST_LOADER.load('[ {"action": "ACCEPT", ' +
' "from": "192.0.2.1"},' +
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment