]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
4357. [func] Add the python RNDC module. [RT #42093]
authorWitold Krecicki <wpk@isc.org>
Thu, 5 May 2016 09:33:47 +0000 (11:33 +0200)
committerWitold Krecicki <wpk@isc.org>
Thu, 5 May 2016 09:33:47 +0000 (11:33 +0200)
CHANGES
bin/python/isc/Makefile.in
bin/python/isc/__init__.py
bin/python/isc/rndc.py [new file with mode: 0644]
bin/tests/system/rndc/tests.sh

diff --git a/CHANGES b/CHANGES
index 588c9fdea5a7ac02533963ca1e66c829df7ce6e3..544efb48f24c887f7b7fbee17eeeba34df9bd07e 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,5 @@
+4357.  [func]          Add the python RNDC module. [RT #42093]
+
 4356.  [func]          Add the ability to specify whether to wait for
                        nameserver addresses to be looked up or not to
                        rpz with a new modifying directive 'nsip-wait-recurse'.
index 0bc39e42cf61c845f8d69539b61a769ce04d47d4..04953990a00e7f7bebb49d0ca134d92b161fdb78 100644 (file)
@@ -23,7 +23,7 @@ SUBDIRS       =       tests
 PYTHON =       @PYTHON@
 
 PYSRCS =       __init__.py dnskey.py eventlist.py keydict.py \
-               keyevent.py keyzone.py policy.py
+               keyevent.py keyzone.py policy.py rndc.py
 TARGETS =      parsetab.py
 
 @BIND9_MAKE_RULES@
index f408af24fb5e4e8cf07f8b038a4141941b8c53da..8ca9ee273d0cb7fb8be2eee78e0586ba023e950a 100644 (file)
@@ -16,7 +16,7 @@
 
 __all__ = ['checkds', 'coverage', 'keymgr', 'dnskey', 'eventlist',
            'keydict', 'keyevent', 'keyseries', 'keyzone', 'policy',
-           'parsetab', 'utils']
+           'parsetab', 'rndc', 'utils']
 
 from isc.dnskey import *
 from isc.eventlist import *
@@ -25,4 +25,5 @@ from isc.keyevent import *
 from isc.keyseries import *
 from isc.keyzone import *
 from isc.policy import *
+from isc.rndc import *
 from isc.utils import *
diff --git a/bin/python/isc/rndc.py b/bin/python/isc/rndc.py
new file mode 100644 (file)
index 0000000..68cf44c
--- /dev/null
@@ -0,0 +1,184 @@
+############################################################################
+# Copyright (C) 2016  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.
+############################################################################
+
+############################################################################
+# rndc.py
+# This module implements the RNDC control protocol.
+############################################################################
+
+from collections import OrderedDict
+from exceptions import TypeError
+import time
+import struct
+import hashlib
+import hmac
+import base64
+import random
+import socket
+
+
+class rndc(object):
+    """RNDC protocol client library"""
+    __algos = {'md5':    157,
+               'sha1':   161,
+               'sha224': 162,
+               'sha256': 163,
+               'sha384': 164,
+               'sha512': 165}
+
+    def __init__(self, host, algo, secret):
+        """Creates a persistent connection to RNDC and logs in
+        host - (ip, port) tuple
+        algo - HMAC algorithm: one of md5, sha1, sha224, sha256, sha384, sha512
+               (with optional prefix 'hmac-')
+        secret - HMAC secret, base64 encoded"""
+        self.host = host
+        algo = algo.lower()
+        if algo.startswith('hmac-'):
+            algo = algo[5:]
+        self.algo = algo
+        self.hlalgo = getattr(hashlib, algo)
+        self.secret = base64.b64decode(secret)
+        self.ser = random.randint(0, 1 << 24)
+        self.nonce = None
+        self.__connect_login()
+
+    def call(self, cmd):
+        """Call a RNDC command, all parsing is done on the server side
+        cmd - a complete string with a command (eg 'reload zone example.com')
+        """
+        return dict(self.__command(type=cmd)['_data'])
+
+    def __serialize_dict(self, data, ignore_auth=False):
+        rv = ''
+        for k, v in data.iteritems():
+            if ignore_auth and k == '_auth':
+                continue
+            rv += chr(len(k))
+            rv += k
+            if type(v) == str:
+                rv += struct.pack('>BI', 1, len(v)) + v
+            elif type(v) == OrderedDict:
+                sd = self.__serialize_dict(v)
+                rv += struct.pack('>BI', 2, len(sd)) + sd
+            else:
+                raise NotImplementedError('Cannot serialize element of type %s'
+                                          % type(v))
+        return rv
+
+    def __prep_message(self, *args, **kwargs):
+        self.ser += 1
+        now = int(time.time())
+        data = OrderedDict(*args, **kwargs)
+
+        d = OrderedDict()
+        d['_auth'] = OrderedDict()
+        d['_ctrl'] = OrderedDict()
+        d['_ctrl']['_ser'] = str(self.ser)
+        d['_ctrl']['_tim'] = str(now)
+        d['_ctrl']['_exp'] = str(now+60)
+        if self.nonce is not None:
+            d['_ctrl']['_nonce'] = self.nonce
+        d['_data'] = data
+
+        msg = self.__serialize_dict(d, ignore_auth=True)
+        hash = hmac.new(self.secret, msg, self.hlalgo).digest()
+        bhash = base64.b64encode(hash)
+        if self.algo == 'md5':
+            d['_auth']['hmd5'] = struct.pack('22s', bhash)
+        else:
+            d['_auth']['hsha'] = struct.pack('B88s',
+                                             self.__algos[self.algo], bhash)
+        msg = self.__serialize_dict(d)
+        msg = struct.pack('>II', len(msg) + 4, 1) + msg
+        return msg
+
+    def __verify_msg(self, msg):
+        if self.nonce is not None and msg['_ctrl']['_nonce'] != self.nonce:
+            return False
+        bhash = msg['_auth']['hmd5' if self.algo == 'md5' else 'hsha']
+        bhash += '=' * (4 - (len(bhash) % 4))
+        remote_hash = base64.b64decode(bhash)
+        my_msg = self.__serialize_dict(msg, ignore_auth=True)
+        my_hash = hmac.new(self.secret, my_msg, self.hlalgo).digest()
+        return (my_hash == remote_hash)
+
+    def __command(self, *args, **kwargs):
+        msg = self.__prep_message(*args, **kwargs)
+        sent = self.socket.send(msg)
+        if sent != len(msg):
+            raise IOError("Cannot send the message")
+
+        header = self.socket.recv(8)
+        if len(header) != 8:
+            # What should we throw here? Bad auth can cause this...
+            raise IOError("Can't read response header")
+
+        length, version = struct.unpack('>II', header)
+        if version != 1:
+            raise NotImplementedError('Wrong message version %d' % version)
+
+        # it includes the header
+        length -= 4
+        data = self.socket.recv(length, socket.MSG_WAITALL)
+        if len(data) != length:
+            raise IOError("Can't read response data")
+
+        msg = self.__parse_message(data)
+        if not self.__verify_msg(msg):
+            raise IOError("Authentication failure")
+
+        return msg
+
+    def __connect_login(self):
+        self.socket = socket.create_connection(self.host)
+        self.nonce = None
+        msg = self.__command(type='null')
+        self.nonce = msg['_ctrl']['_nonce']
+
+    def __parse_element(self, input):
+        pos = 0
+        labellen = ord(input[pos])
+        pos += 1
+        label = input[pos:pos+labellen]
+        pos += labellen
+        type = ord(input[pos])
+        pos += 1
+        datalen = struct.unpack('>I', input[pos:pos+4])[0]
+        pos += 4
+        data = input[pos:pos+datalen]
+        pos += datalen
+        rest = input[pos:]
+
+        if type == 1:         # raw binary value
+            return label, data, rest
+        elif type == 2:       # dictionary
+            d = OrderedDict()
+            while len(data) > 0:
+                ilabel, value, data = self.__parse_element(data)
+                d[ilabel] = value
+            return label, d, rest
+        # TODO type 3 - list
+        else:
+            raise NotImplementedError('Unknown element type %d' % type)
+
+    def __parse_message(self, input):
+        rv = OrderedDict()
+        hdata = None
+        while len(input) > 0:
+            label, value, input = self.__parse_element(input)
+            rv[label] = value
+        return rv
index 80113432dad497914156a75f4dfa74d733d75e08..bc0b755fb1031a1e5475f322778d53030d6bff26 100644 (file)
@@ -448,4 +448,20 @@ grep "^running on " rndc.output > /dev/null || ret=1
 if [ $ret != 0 ]; then echo "I:failed"; fi
 status=`expr $status + $ret`
 
+if [ -x "$PYTHON" ]; then
+    echo "I:test rndc python bindings"
+    ret=0
+    $PYTHON > rndc.output << EOF
+import sys
+sys.path.insert(0, '../../../../bin/python')
+from isc import *
+r = rndc(('10.53.0.5', 9953), 'hmac-sha256', '1234abcd8765')
+result = r.call('status')
+print(result['text'])
+EOF
+    grep 'server is up and running' rndc.output > /dev/null 2>&1 || ret=1
+    if [ $ret != 0 ]; then echo "I:failed"; fi
+    status=`expr $status + $ret`
+fi
+
 exit $status